Compare commits

..
Author SHA1 Message Date
Bill He ca6b266bb2 bump abacus 2023-12-15 14:18:52 -08:00
Jared Vu 3f10e045f4 bump abacus & wagmi (#196) 2023-12-15 09:26:41 -08:00
Bill 71d1a0321b Do not block website if NobleClient cannot init, and other onboarding fixes (#193) 2023-12-13 08:11:19 -08:00
Bill 0f16396c8d Implement CCTP withdraw (#188)
* Implement CCTP withdraw

* bump packages

* address comments
2023-12-12 10:12:37 -08:00
Jared Vu 2709d79f59 Firefox issues (#190)
* remove row flicker on hover

* Fix border for AccountInfoConnectedState on mobile

* Remove sticky position to allow button click

* Fix: AccountInfo not displaying on mobile firefox

* Fix css var name

* Restore filter on Table row hover

* Remove PerspectiveArea. broken on firefox

* Symbol no longer exists on AbacusType

* HorizontalPanel: memoize tabConfig

* input: firefox cant click between chars
2023-12-12 08:37:50 -08:00
Bill ab0eec5d4f Update noble tesnet rpc (#192) 2023-12-12 11:35:21 -05:00
Jared Vu 4a044da38f Size inputs (#191)
* use formattedValue to update redux state

* Separate updateLeverage and onLeverageInput

* Remove console.log
2023-12-12 07:11:51 -08:00
Jared Vu fbbfbc6580 Bump v4-abacus and v4-localization (#189) 2023-12-08 15:41:07 -08:00
Rui 2714c2b084 Fix path to chainLogo (#187) 2023-12-07 13:03:44 -08:00
Jared Vu 9979bc8a16 Prevent Spread row from moving when orderbook updates (#186)
* Add dummy rows to ensure even row count

* Reverse bids for horizontal layout

* disable pointer-events for dummy rows
2023-12-06 10:26:47 -08:00
aleka a958a59ea2 Display more markets 12/06 (#178)
* add more markets 12/06
2023-12-06 11:38:49 -05:00
Jared Vu 2bf9e66da5 Fix inputs for comma locales (#171)
* Fix for comma decimal separator locales

* Accept tickSizeDecimals 0

* Use FloatValue as value for Size Input

* Use formattedValue to handle prefix/suffix

* Use MustBigNumber.toString()
2023-12-05 16:22:41 -08:00
Bill 2980144e40 Enable CCTP via TestFlags (#185)
* Enable CCTP via Env Var

* Use test flags instead
2023-12-05 14:48:47 -08:00
aleka 5387fe8632 Display more markets 12/05 (#177)
* add more markets 12/05
2023-12-05 09:52:12 -05:00
Bill 1132ea3bbd Do not init nobleClient if nobleValidatorUrl does not exist (#184) 2023-12-04 11:19:29 -08:00
aleka 9188e08a4f prevent 1password fill (#183) 2023-12-04 11:06:49 -05:00
aleka 941ef7f9dd Display more markets 12/04 (#176)
* add more markets 11/30

* add more markets 12/01

* add more markets 12/04
2023-12-04 10:53:25 -05:00
Bill b1ffa2e219 Support CCTP and noble auto deposits on testnet (#180)
* Support CCTP and noble auto deposits on testnet

* slippage

* fix fees

* update isCctp, and comments

* bump packages

* cctp.json

* Bump abacus

* fix error

* fix loading button
2023-12-01 15:18:50 -08:00
aleka 91ea68dc53 Add chaos labs launch incentives panel (#182)
* add chaos lab incentives panel

* add seen state

* external link dialog

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

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

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

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

* fix mobile nav menu nested buttons

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

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

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

* Move sideEffects from TradeForm to hook in Trade
2023-11-20 11:56:09 -08:00
aleka fb65db633a move buttons to footer in ack terms (#155) 2023-11-20 13:21:06 -05:00
aleka 279ef6b4f9 bump abacus (#153) 2023-11-13 10:57:40 -05:00
aleka 0def18db07 fix sorting 24h price change in markets table (#152) 2023-11-13 09:36:32 -05:00
Jared Vu 50bd860ced Bump Abacus 1.0.25 (#151) 2023-11-13 09:36:16 -05:00
Jared Vu 818574cedb Change dep in hook to fix input bug (#150) 2023-11-13 09:11:54 -05:00
96 changed files with 2956 additions and 949 deletions
+4 -4
View File
@@ -39,9 +39,9 @@
"@cosmjs/proto-signing": "^0.31.0",
"@cosmjs/stargate": "^0.31.0",
"@cosmjs/tendermint-rpc": "^0.31.0",
"@dydxprotocol/v4-abacus": "^1.0.24",
"@dydxprotocol/v4-client-js": "^1.0.0",
"@dydxprotocol/v4-localization": "^1.0.5",
"@dydxprotocol/v4-abacus": "^1.1.24",
"@dydxprotocol/v4-client-js": "^1.0.11",
"@dydxprotocol/v4-localization": "^1.0.18",
"@ethersproject/providers": "^5.7.2",
"@js-joda/core": "^5.5.3",
"@radix-ui/react-checkbox": "^1.0.4",
@@ -111,7 +111,7 @@
"styled-components": "^5.3.11",
"use-latest": "^1.2.1",
"viem": "^1.12.2",
"wagmi": "^1.4.5"
"wagmi": "^1.4.12"
},
"devDependencies": {
"@babel/core": "^7.22.5",
+670 -127
View File
File diff suppressed because it is too large Load Diff
+41
View File
@@ -0,0 +1,41 @@
[
{
"chainId": "1",
"tokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"name": "Ethereum"
},
{
"chainId": "421613",
"tokenAddress": "0xfd064a18f3bf249cf1f87fc203e90d8f650f2d63",
"name": "Arbitrum Goerli"
},
{
"chainId": "43113",
"tokenAddress": "0x5425890298aed601595a70AB815c96711a31Bc65",
"name": "Avalanche Fuji"
},
{
"chainId": "84531",
"tokenAddress": "0xf175520c52418dfe19c8098071a252da48cd1c19",
"name": "Base Goerli"
},
{
"chainId": "5",
"tokenAddress": "0x07865c6E87B9F70255377e024ace6630C1Eaa37F",
"name": "Ethereum Goerli"
},
{
"chainId": "grand-1",
"tokenAddress": "uusdc",
"name": "Noble Testnet"
},
{
"chainId": "420",
"tokenAddress": "0xe05606174bac4a6364b31bd0eca4bf4dd368f8c6",
"name": "OP Goerli"
},
{
"chainId": "103",
"name": "Solana Devnet"
}
]
+38 -22
View File
@@ -42,7 +42,7 @@
"ethereumChainId": "5",
"dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
@@ -71,6 +71,7 @@
"https://validator.v4dev.dydx.exchange"
],
"0xsquid": "https://testnet.api.0xsquid.com",
"nobleValidator": "https://noble-testnet-rpc.polkachu.com/",
"faucet": "https://faucet.v4dev.dydx.exchange"
},
"links": {
@@ -115,7 +116,7 @@
"ethereumChainId": "5",
"dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
@@ -143,7 +144,8 @@
"validators": [
"http://54.92.118.111"
],
"0xsquid": "https://testnet.api.0xsquid.com"
"0xsquid": "https://testnet.api.0xsquid.com",
"nobleValidator": "https://noble-testnet-rpc.polkachu.com/"
},
"links": {
"tos": "https://dydx.exchange/v4-terms",
@@ -187,7 +189,7 @@
"ethereumChainId": "5",
"dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
@@ -215,7 +217,8 @@
"validators": [
"http://validator.v4dev4.dydx.exchange"
],
"0xsquid": "https://testnet.api.0xsquid.com"
"0xsquid": "https://testnet.api.0xsquid.com",
"nobleValidator": "https://noble-testnet-rpc.polkachu.com/"
},
"links": {
"tos": "https://dydx.exchange/v4-terms",
@@ -259,7 +262,7 @@
"ethereumChainId": "5",
"dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
@@ -287,7 +290,8 @@
"validators": [
"http://18.223.78.50"
],
"0xsquid": "https://testnet.api.0xsquid.com"
"0xsquid": "https://testnet.api.0xsquid.com",
"nobleValidator": "https://noble-testnet-rpc.polkachu.com/"
},
"links": {
"tos": "https://dydx.exchange/v4-terms",
@@ -331,7 +335,7 @@
"ethereumChainId": "5",
"dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
@@ -360,7 +364,8 @@
"validators": [
"https://validator.v4staging.dydx.exchange"
],
"0xsquid": "https://testnet.api.squidrouter.com"
"0xsquid": "https://testnet.api.squidrouter.com",
"nobleValidator": "https://noble-testnet-rpc.polkachu.com/"
},
"links": {
"tos": "https://dydx.exchange/v4-terms",
@@ -405,7 +410,8 @@
"ethereumChainId": "5",
"dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
"chain": {
@@ -433,7 +439,8 @@
"validators": [
"https://validator.v4staging.dydx.exchange"
],
"0xsquid": "https://testnet.api.squidrouter.com"
"0xsquid": "https://testnet.api.squidrouter.com",
"nobleValidator": "https://noble-testnet-rpc.polkachu.com/"
},
"links": {
"tos": "https://dydx.exchange/v4-terms",
@@ -476,7 +483,7 @@
"ethereumChainId": "5",
"dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
@@ -505,7 +512,8 @@
"validators": [
"https://validator-uswest1.v4staging.dydx.exchange"
],
"0xsquid": "https://testnet.api.squidrouter.com"
"0xsquid": "https://testnet.api.squidrouter.com",
"nobleValidator": "https://noble-testnet-rpc.polkachu.com/"
},
"links": {
"tos": "https://dydx.exchange/v4-terms",
@@ -550,7 +558,7 @@
"ethereumChainId": "5",
"dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
@@ -583,6 +591,7 @@
"https://dydx-rpc.liquify.com/api=8878132/dydx"
],
"0xsquid": "https://testnet.api.squidrouter.com",
"nobleValidator": "https://noble-testnet-rpc.polkachu.com/",
"faucet": "https://faucet.v4testnet.dydx.exchange"
},
"links": {
@@ -628,7 +637,7 @@
"ethereumChainId": "5",
"dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
@@ -657,6 +666,7 @@
"https://validator.v4testnet.dydx.exchange"
],
"0xsquid": "https://testnet.api.squidrouter.com",
"nobleValidator": "https://noble-testnet-rpc.polkachu.com/",
"faucet": "https://faucet.v4testnet.dydx.exchange"
},
"links": {
@@ -703,7 +713,7 @@
"ethereumChainId": "5",
"dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
@@ -732,6 +742,7 @@
"https://dydx-testnet.nodefleet.org"
],
"0xsquid": "https://testnet.api.squidrouter.com",
"nobleValidator": "https://noble-testnet-rpc.polkachu.com/",
"faucet": "https://faucet.v4testnet.dydx.exchange"
},
"links": {
@@ -778,7 +789,7 @@
"ethereumChainId": "5",
"dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
@@ -807,6 +818,7 @@
"https://test-dydx.kingnodes.com"
],
"0xsquid": "https://testnet.api.squidrouter.com",
"nobleValidator": "https://noble-testnet-rpc.polkachu.com/",
"faucet": "https://faucet.v4testnet.dydx.exchange"
},
"links": {
@@ -853,7 +865,7 @@
"ethereumChainId": "5",
"dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
@@ -882,6 +894,7 @@
"https://dydx-rpc.liquify.com/api=8878132/dydx"
],
"0xsquid": "https://testnet.api.squidrouter.com",
"nobleValidator": "https://noble-testnet-rpc.polkachu.com/",
"faucet": "https://faucet.v4testnet.dydx.exchange"
},
"links": {
@@ -928,7 +941,7 @@
"ethereumChainId": "5",
"dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
@@ -957,6 +970,7 @@
"https://dydx-testnet-rpc.polkachu.com/"
],
"0xsquid": "https://testnet.api.squidrouter.com",
"nobleValidator": "https://noble-testnet-rpc.polkachu.com/",
"faucet": "https://faucet.v4testnet.dydx.exchange"
},
"links": {
@@ -994,7 +1008,7 @@
"ethereumChainId": "5",
"dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"chainLogo": "/dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
@@ -1023,6 +1037,7 @@
"https://dydx-testnet-full-rpc.public.blastapi.io/"
],
"0xsquid": "https://testnet.api.squidrouter.com",
"nobleValidator": "https://noble-testnet-rpc.polkachu.com/",
"faucet": "https://faucet.v4testnet.dydx.exchange"
},
"links": {
@@ -1069,7 +1084,7 @@
"ethereumChainId": "1",
"dydxChainId": "[mainnet chain id]",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"chainLogo": "/dydx-chain.png",
"squidIntegratorId": "[mainnet squid integrator id]",
"isMainNet": true,
"tokens": {
@@ -1099,7 +1114,8 @@
"[Validator endpoint 1",
"[Validator endpoint n]"
],
"0xsquid": "[0xSquid endpoint for mainnet]"
"0xsquid": "[0xSquid endpoint for mainnet]",
"nobleValidator": "[noble validator endpoint for mainnet]"
},
"links": {
"tos": "[HTTP link to TOS]",
+1 -1
View File
@@ -199,7 +199,7 @@
},
"mobile": {
"native": "trust:",
"universal": "https://link.trustwallet.com"
"universal": ""
},
"desktop": {
"native": "",
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 300 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 481 KiB

-4
View File
@@ -22,8 +22,6 @@ import { NotificationsProvider } from '@/hooks/useNotifications';
import { LocalNotificationsProvider } from '@/hooks/useLocalNotifications';
import { RestrictionProvider } from '@/hooks/useRestrictions';
import { SubaccountProvider } from '@/hooks/useSubaccount';
import { SquidProvider } from '@/hooks/useSquid';
import { TestFlagsProvider } from '@/hooks/useTestFlags';
import { GuardedMobileRoute } from '@/components/GuardedMobileRoute';
@@ -124,13 +122,11 @@ const providers = [
wrapProvider(QueryClientProvider, { client: queryClient }),
wrapProvider(GrazProvider),
wrapProvider(WagmiConfig, { config }),
wrapProvider(TestFlagsProvider),
wrapProvider(LocaleProvider),
wrapProvider(RestrictionProvider),
wrapProvider(DydxProvider),
wrapProvider(AccountsProvider),
wrapProvider(SubaccountProvider),
wrapProvider(SquidProvider),
wrapProvider(LocalNotificationsProvider),
wrapProvider(NotificationsProvider),
wrapProvider(DialogAreaProvider),
+390
View File
@@ -0,0 +1,390 @@
[
{
"constant": true,
"inputs": [],
"name": "name",
"outputs": [{ "name": "", "type": "string" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [{ "name": "_upgradedAddress", "type": "address" }],
"name": "deprecate",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{ "name": "_spender", "type": "address" },
{ "name": "_value", "type": "uint256" }
],
"name": "approve",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "deprecated",
"outputs": [{ "name": "", "type": "bool" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [{ "name": "_evilUser", "type": "address" }],
"name": "addBlackList",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "totalSupply",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{ "name": "_from", "type": "address" },
{ "name": "_to", "type": "address" },
{ "name": "_value", "type": "uint256" }
],
"name": "transferFrom",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "upgradedAddress",
"outputs": [{ "name": "", "type": "address" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [{ "name": "", "type": "address" }],
"name": "balances",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "decimals",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "maximumFee",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "_totalSupply",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [],
"name": "unpause",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [{ "name": "_maker", "type": "address" }],
"name": "getBlackListStatus",
"outputs": [{ "name": "", "type": "bool" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "name": "", "type": "address" },
{ "name": "", "type": "address" }
],
"name": "allowed",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "paused",
"outputs": [{ "name": "", "type": "bool" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [{ "name": "who", "type": "address" }],
"name": "balanceOf",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [],
"name": "pause",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "getOwner",
"outputs": [{ "name": "", "type": "address" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "owner",
"outputs": [{ "name": "", "type": "address" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "symbol",
"outputs": [{ "name": "", "type": "string" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{ "name": "_to", "type": "address" },
{ "name": "_value", "type": "uint256" }
],
"name": "transfer",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{ "name": "newBasisPoints", "type": "uint256" },
{ "name": "newMaxFee", "type": "uint256" }
],
"name": "setParams",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [{ "name": "amount", "type": "uint256" }],
"name": "issue",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [{ "name": "amount", "type": "uint256" }],
"name": "redeem",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "name": "_owner", "type": "address" },
{ "name": "_spender", "type": "address" }
],
"name": "allowance",
"outputs": [{ "name": "remaining", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "basisPointsRate",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [{ "name": "", "type": "address" }],
"name": "isBlackListed",
"outputs": [{ "name": "", "type": "bool" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [{ "name": "_clearedUser", "type": "address" }],
"name": "removeBlackList",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "MAX_UINT",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [{ "name": "newOwner", "type": "address" }],
"name": "transferOwnership",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [{ "name": "_blackListedUser", "type": "address" }],
"name": "destroyBlackFunds",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{ "name": "_initialSupply", "type": "uint256" },
{ "name": "_name", "type": "string" },
{ "name": "_symbol", "type": "string" },
{ "name": "_decimals", "type": "uint256" }
],
"payable": false,
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [{ "indexed": false, "name": "amount", "type": "uint256" }],
"name": "Issue",
"type": "event"
},
{
"anonymous": false,
"inputs": [{ "indexed": false, "name": "amount", "type": "uint256" }],
"name": "Redeem",
"type": "event"
},
{
"anonymous": false,
"inputs": [{ "indexed": false, "name": "newAddress", "type": "address" }],
"name": "Deprecate",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{ "indexed": false, "name": "feeBasisPoints", "type": "uint256" },
{ "indexed": false, "name": "maxFee", "type": "uint256" }
],
"name": "Params",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{ "indexed": false, "name": "_blackListedUser", "type": "address" },
{ "indexed": false, "name": "_balance", "type": "uint256" }
],
"name": "DestroyedBlackFunds",
"type": "event"
},
{
"anonymous": false,
"inputs": [{ "indexed": false, "name": "_user", "type": "address" }],
"name": "AddedBlackList",
"type": "event"
},
{
"anonymous": false,
"inputs": [{ "indexed": false, "name": "_user", "type": "address" }],
"name": "RemovedBlackList",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{ "indexed": true, "name": "owner", "type": "address" },
{ "indexed": true, "name": "spender", "type": "address" },
{ "indexed": false, "name": "value", "type": "uint256" }
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{ "indexed": true, "name": "from", "type": "address" },
{ "indexed": true, "name": "to", "type": "address" },
{ "indexed": false, "name": "value", "type": "uint256" }
],
"name": "Transfer",
"type": "event"
},
{ "anonymous": false, "inputs": [], "name": "Pause", "type": "event" },
{ "anonymous": false, "inputs": [], "name": "Unpause", "type": "event" }
]
+1 -1
View File
@@ -61,7 +61,7 @@ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonPr
return (
<StyledBaseButton
disabled={state[ButtonState.Disabled]}
disabled={state[ButtonState.Disabled] || state[ButtonState.Loading]}
{...{ ref, action, size, shape, state, ...otherProps }}
>
{
+7 -5
View File
@@ -31,10 +31,12 @@ type ElementProps<TabItemsValue> = {
};
type StyleProps = {
fullWidthTabs?: boolean;
className?: string;
fullWidthTabs?: boolean;
};
export type CollapsibleTabsProps<TabItemsValue> = ElementProps<TabItemsValue> & StyleProps;
export const CollapsibleTabs = <TabItemsValue extends string>({
defaultValue,
items,
@@ -43,8 +45,9 @@ export const CollapsibleTabs = <TabItemsValue extends string>({
onOpenChange,
fullWidthTabs,
className,
}: ElementProps<TabItemsValue> & StyleProps) => {
}: CollapsibleTabsProps<TabItemsValue>) => {
const [value, setValue] = useState(defaultValue);
const currentItem = items.find((item) => item.value === value);
@@ -86,8 +89,8 @@ export const CollapsibleTabs = <TabItemsValue extends string>({
</Styled.Header>
<Styled.CollapsibleContent>
{items.map(({ value, content }) => (
<Styled.TabsContent key={value} value={value}>
{items.map(({ asChild, value, content }) => (
<Styled.TabsContent key={value} asChild={asChild} value={value}>
{content}
</Styled.TabsContent>
))}
@@ -219,7 +222,6 @@ Styled.Header = styled.header`
Styled.CollapsibleContent = styled(CollapsibleContent)`
${layoutMixins.stack}
${layoutMixins.perspectiveArea}
box-shadow: none;
`;
+1
View File
@@ -122,6 +122,7 @@ Styled.Dialog = styled(Dialog)`
/* Net 0 sticky top inset (let stickyArea1 header stick to top) */
--stickyArea0-topGap: calc(-1 * var(--stickyArea0-topHeight));
overflow-x: clip;
}
`;
+6 -9
View File
@@ -44,13 +44,6 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
}: ComboboxMenuProps<MenuItemValue, MenuGroupValue>) => {
const [highlightedCommand, setHighlightedCommand] = useState<MenuItemValue>();
const [searchValue, setSearchValue] = useState('');
// const inputRef = useRef<HTMLInputElement | null>(null);
// console.log({ commandValue: highlightedCommand });
// useEffect(() => {
// inputRef?.current?.focus();
// }, []);
return (
<Styled.Command
@@ -67,11 +60,14 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
{withSearch && (
<Styled.Header $withStickyLayout={withStickyLayout}>
<Styled.Input
// ref={inputRef}
/**
* Mobile Issue: Search Input will always trigger mobile keyboard drawer. There is no fix.
* https://github.com/pacocoursey/cmdk/issues/127
*/
autoFocus
type="search"
value={searchValue}
onValueChange={setSearchValue}
autoFocus
placeholder={inputPlaceholder}
/>
</Styled.Header>
@@ -258,6 +254,7 @@ Styled.Group = styled(Command.Group)<{ $withItemBorders?: boolean; $withStickyLa
> [cmdk-group-heading] {
${layoutMixins.stickyHeader}
z-index: 2;
}
> [cmdk-group-items] {
+12 -7
View File
@@ -10,6 +10,7 @@ import { layoutMixins } from '@/styles/layoutMixins';
import { Button, ButtonProps } from './Button';
import { Icon, IconName } from './Icon';
import { IconButton } from './IconButton';
import { WithTooltip } from './WithTooltip';
export type CopyButtonProps = {
value?: string;
@@ -40,13 +41,17 @@ export const CopyButton = ({
<Styled.Icon copied={copied} iconName={copied ? IconName.Check : IconName.Copy} />
</Styled.InlineRow>
) : buttonType === 'icon' ? (
<Styled.IconButton
{...buttonProps}
copied={copied}
action={ButtonAction.Base}
iconName={copied ? IconName.Check : IconName.Copy}
onClick={onCopy}
/>
<WithTooltip
tooltipString={stringGetter({ key: copied ? STRING_KEYS.COPIED : STRING_KEYS.COPY })}
>
<Styled.IconButton
{...buttonProps}
copied={copied}
action={ButtonAction.Base}
iconName={copied ? IconName.Check : IconName.Copy}
onClick={onCopy}
/>
</WithTooltip>
) : (
<Button
{...buttonProps}
+6
View File
@@ -38,6 +38,7 @@ import {
HelpCircleIcon,
HideIcon,
HistoryIcon,
LeaderboardIcon,
LinkOutIcon,
LockIcon,
LogoShortIcon,
@@ -53,6 +54,7 @@ import {
OrderUntriggeredIcon,
OverviewIcon,
PencilIcon,
PlayIcon,
PositionsIcon,
PriceChartIcon,
PrivacyIcon,
@@ -110,6 +112,7 @@ export enum IconName {
HelpCircle = 'HelpCircle',
Hide = 'Hide',
History = 'History',
Leaderboard = 'Leaderboard',
LinkOut = 'LinkOut',
Lock = 'Lock',
LogoShort = 'LogoShort',
@@ -126,6 +129,7 @@ export enum IconName {
OrderUntriggered = 'OrderUntriggered',
Overview = 'Overview',
Pencil = 'Pencil',
Play = 'Play',
Positions = 'Positions',
PriceChart = 'PriceChart',
Privacy = 'Privacy',
@@ -183,6 +187,7 @@ const icons = {
[IconName.HelpCircle]: HelpCircleIcon,
[IconName.Hide]: HideIcon,
[IconName.History]: HistoryIcon,
[IconName.Leaderboard]: LeaderboardIcon,
[IconName.LinkOut]: LinkOutIcon,
[IconName.Lock]: LockIcon,
[IconName.LogoShort]: LogoShortIcon,
@@ -198,6 +203,7 @@ const icons = {
[IconName.OrderUntriggered]: OrderUntriggeredIcon,
[IconName.Overview]: OverviewIcon,
[IconName.Pencil]: PencilIcon,
[IconName.Play]: PlayIcon,
[IconName.Positions]: PositionsIcon,
[IconName.PriceChart]: PriceChartIcon,
[IconName.Privacy]: PrivacyIcon,
+30 -21
View File
@@ -10,7 +10,7 @@ import {
USD_DECIMALS,
} from '@/constants/numbers';
import { BIG_NUMBERS, MustBigNumber } from '@/lib/numbers';
import { BIG_NUMBERS } from '@/lib/numbers';
import { useLocaleSeparators } from '@/hooks';
export enum InputType {
@@ -29,31 +29,38 @@ type StyleProps = {
type ElementProps = {
type?: InputType;
value?: string | number | null;
allowNegative?: boolean;
decimals?: number;
disabled?: boolean;
id?: string;
max?: number;
onBlur?: () => void;
onChange?:
| Dispatch<SetStateAction<string>>
| React.ReactEventHandler<HTMLInputElement>
| ((values: NumberFormatValues, e: SourceInfo) => void);
onFocus?: () => void;
onInput?: ({
value,
floatValue,
formattedValue,
}: {
value: string;
floatValue?: number;
formattedValue: string;
}) => void;
placeholder?: string;
};
export type InputProps = ElementProps & StyleProps;
type ConditionalProps =
| {
allowNegative?: boolean;
decimals?: number;
max?: number;
onChange?: (values: NumberFormatValues, e: SourceInfo) => void;
onInput?: ({
value,
floatValue,
formattedValue,
}: {
value: string;
floatValue?: number;
formattedValue: string;
}) => void;
}
| {
allowNegative?: never;
decimals?: never;
max?: never;
onChange?: Dispatch<SetStateAction<string>> | React.ReactEventHandler<HTMLInputElement>;
onInput?: (e: SyntheticInputEvent) => void;
};
export type InputProps = ElementProps & StyleProps & ConditionalProps;
export const Input = forwardRef<HTMLInputElement, InputProps>(
(
@@ -129,6 +136,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
placeholder={placeholder}
value={value}
// Other
data-1p-ignore // prevent 1Password fill
{...otherProps}
/>
) : (
@@ -137,6 +145,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
ref={ref as React.Ref<typeof NumericFormat<unknown>>}
id={id}
// NumericFormat
valueIsNumericString
allowNegative={allowNegative}
decimalScale={decimals}
decimalSeparator={LOCALE_DECIMAL_SEPARATOR}
@@ -160,7 +169,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
? undefined
: Number(formattedValue.replace(',', '.'));
onInput?.({ value, floatValue, formattedValue });
onInput?.({ value, floatValue, formattedValue, ...e });
}}
// Native
disabled={disabled}
@@ -168,6 +177,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
value={formattedValue}
autoComplete="off"
autoCorrect="off"
data-1p-ignore // prevent 1Password fill
{...otherProps}
/>
)}
@@ -188,7 +198,6 @@ Styled.InputContainer = styled.div`
border-radius: inherit;
input {
user-select: all;
flex: 1;
width: 100%;
}
+5 -2
View File
@@ -21,6 +21,7 @@ export type TabItem<TabItemsValue> = {
content?: React.ReactNode;
subitems?: TabItem<TabItemsValue>[];
customTrigger?: ReactNode;
asChild?: boolean;
};
type ElementProps<TabItemsValue> = {
@@ -41,6 +42,8 @@ type StyleProps = {
className?: string;
};
export type TabsProps<TabItemsValue> = ElementProps<TabItemsValue> & StyleProps;
export const Tabs = <TabItemsValue extends string>({
defaultValue,
value,
@@ -107,9 +110,10 @@ export const Tabs = <TabItemsValue extends string>({
sharedContent
) : (
<Styled.Stack>
{items.map(({ value, content, forceMount }) => (
{items.map(({ asChild, value, content, forceMount }) => (
<Styled.Content
key={value}
asChild={asChild}
value={value}
forceMount={forceMount}
$hide={forceMount && currentItem?.value !== value}
@@ -242,7 +246,6 @@ Styled.Trigger = styled(Trigger)<{ $withBorders?: boolean }>`
Styled.Stack = styled.div`
${layoutMixins.stack}
${layoutMixins.perspectiveArea}
box-shadow: none;
`;
+7 -2
View File
@@ -4,6 +4,7 @@ import { Root, Action, Close } from '@radix-ui/react-toast';
import { ButtonShape, ButtonSize } from '@/constants/buttons';
import { popoverMixins } from '@/styles/popoverMixins';
import { breakpoints } from '@/styles';
import { Notification, type NotificationProps } from '@/components/Notification';
@@ -76,7 +77,7 @@ export const Toast = ({
<Close asChild>
<$CloseButton
iconName={IconName.Close}
shape={ButtonShape.Square}
shape={ButtonShape.Circle}
size={ButtonSize.XSmall}
onClick={(e: MouseEvent) => e.stopPropagation()}
/>
@@ -249,12 +250,16 @@ const $CloseButton = styled(IconButton)`
right: 0;
border: solid var(--border-width) var(--color-border);
border-radius: 50%;
${$Root}:hover & {
display: block;
z-index: 2;
}
@media ${breakpoints.mobile} {
display: block;
z-index: 2;
}
`;
const $Action = styled(Action)`
+5 -2
View File
@@ -1,6 +1,7 @@
import styled from 'styled-components';
import { Provider, Viewport } from '@radix-ui/react-toast';
import styled from 'styled-components';
import { layoutMixins } from '@/styles/layoutMixins';
type ElementProps = {
@@ -12,7 +13,9 @@ type StyleProps = {
className?: string;
};
export const ToastArea = ({ swipeDirection, children, className }: ElementProps & StyleProps) => (
type ToastAreaProps = ElementProps & StyleProps;
export const ToastArea = ({ swipeDirection, children, className }: ToastAreaProps) => (
<$ToastArea className={className}>
<Provider swipeDirection={swipeDirection}>
{children}
+1 -1
View File
@@ -3,6 +3,7 @@ export enum DialogTypes {
Deposit = 'Deposit',
DisconnectWallet = 'DisconnectWallet',
ExchangeOffline = 'ExchangeOffline',
ExternalLink = 'ExternalLink',
FillDetails = 'FillDetails',
Help = 'Help',
ExternalNavKeplr = 'ExternalNavKeplr',
@@ -12,7 +13,6 @@ export enum DialogTypes {
OrderDetails = 'OrderDetails',
Preferences = 'Preferences',
RateLimit = 'RateLimit',
Receive = 'Receive',
RestrictedGeo = 'RestrictedGeo',
RestrictedWallet = 'RestrictedWallet',
Trade = 'Trade',
+1
View File
@@ -22,6 +22,7 @@ export enum LocalStorageKey {
SelectedTheme = 'dydx.SelectedTheme',
SelectedTradeLayout = 'dydx.SelectedTradeLayout',
TradingViewChartConfig = 'dydx.TradingViewChartConfig',
HasSeenLaunchIncentives = 'dydx.HasSeenLaunchIncentives',
}
export const LOCAL_STORAGE_VERSIONS = {
+36
View File
@@ -26,3 +26,39 @@ export enum FundingDirection {
ToShort = 'ToShort',
ToLong = 'ToLong',
}
export const MARKETS_TO_DISPLAY = [
'BTC-USD',
'ETH-USD',
'LINK-USD',
'SOL-USD',
'MATIC-USD',
'ATOM-USD',
'AVAX-USD',
'APE-USD',
'XRP-USD',
'UNI-USD',
'ADA-USD',
'TRX-USD',
'OP-USD',
'MKR-USD',
'DOGE-USD',
'SHIB-USD',
'COMP-USD',
'LDO-USD',
'NEAR-USD',
'APT-USD',
'SUI-USD',
'DOT-USD',
'ETC-USD',
'ARB-USD',
'CRV-USD',
'BLUR-USD',
'FIL-USD',
'XLM-USD',
'PEPE-USD',
'WLD-USD',
'SEI-USD',
'LTC-USD',
'BCH-USD',
];
+8
View File
@@ -4,6 +4,7 @@ import { StatusResponse } from '@0xsquid/sdk';
export enum NotificationType {
AbacusGenerated = 'AbacusGenerated',
SquidTransfer = 'SquidTransfer',
ReleaseUpdates = 'ReleaseUpdates',
}
export enum NotificationComponentType {}
@@ -121,13 +122,20 @@ export type NotificationDisplayData = {
toastDuration?: number;
};
export enum TransferNotificationTypes {
Withdrawal = 'withdrawal',
Deposit = 'deposit',
}
// Notification types
export type TransferNotifcation = {
txHash: string;
type?: TransferNotificationTypes;
toChainId?: string;
fromChainId?: string;
toAmount?: number;
triggeredAt?: number;
isCctp?: boolean;
errorCount?: number;
status?: StatusResponse;
};
+12
View File
@@ -116,3 +116,15 @@ export enum MobilePlaceOrderSteps {
PlacingOrder = 'PlacingOrder',
Confirmation = 'Confirmation',
}
export const CLEARED_TRADE_INPUTS = {
limitPriceInput: '',
triggerPriceInput: '',
trailingPercentInput: '',
};
export const CLEARED_SIZE_INPUTS = {
amountInput: '',
usdAmountInput: '',
leverageInput: '',
};
+2
View File
@@ -22,6 +22,7 @@ import { useShouldShowFooter } from './useShouldShowFooter';
import { useSelectedNetwork } from './useSelectedNetwork';
import { useStringGetter } from './useStringGetter';
import { useSubaccount } from './useSubaccount';
import { useTradeFormInputs } from './useTradeFormInputs';
import { useURLConfigs } from './useURLConfigs';
export {
@@ -49,5 +50,6 @@ export {
useSelectedNetwork,
useStringGetter,
useSubaccount,
useTradeFormInputs,
useURLConfigs,
};
+11 -1
View File
@@ -2,7 +2,7 @@ import { useCallback, useContext, createContext, useEffect, useState, useMemo }
import { useDispatch } from 'react-redux';
import { AES, enc } from 'crypto-js';
import { LocalWallet, type Subaccount } from '@dydxprotocol/v4-client-js';
import { NOBLE_BECH32_PREFIX, LocalWallet, type Subaccount } from '@dydxprotocol/v4-client-js';
import { OnboardingGuard, OnboardingState, type EvmDerivedAddresses } from '@/constants/account';
import { DialogTypes } from '@/constants/dialogs';
@@ -219,6 +219,16 @@ const useAccountsContext = () => {
else abacusStateManager.attemptDisconnectAccount();
}, [localDydxWallet]);
useEffect(() => {
const setNobleWallet = async () => {
if (hdKey?.mnemonic) {
const nobleWallet = await LocalWallet.fromMnemonic(hdKey.mnemonic, NOBLE_BECH32_PREFIX);
abacusStateManager.setNobleWallet(nobleWallet);
}
};
setNobleWallet();
}, [hdKey?.mnemonic]);
// clear subaccounts when no dydxAddress is set
useEffect(() => {
(async () => {
+1
View File
@@ -23,6 +23,7 @@ export const useCurrentMarketId = () => {
const selectedNetwork = useSelector(getSelectedNetwork);
const marketIds = useSelector(getMarketIds, shallowEqual);
const hasMarketIds = marketIds.length > 0;
const [lastViewedMarket, setLastViewedMarket] = useLocalStorage({
key: LocalStorageKey.LastViewedMarket,
defaultValue: DEFAULT_MARKETID,
+11 -9
View File
@@ -1,11 +1,11 @@
import { createContext, useContext, useCallback, useEffect, useMemo } from 'react';
import { useQuery } from 'react-query';
import type { StatusResponse } from '@0xsquid/sdk';
import { LOCAL_STORAGE_VERSIONS, LocalStorageKey } from '@/constants/localStorage';
import { type TransferNotifcation } from '@/constants/notifications';
import { useAccounts } from '@/hooks/useAccounts';
import { STATUS_ERROR_GRACE_PERIOD, useSquid } from '@/hooks/useSquid';
import { fetchSquidStatus, STATUS_ERROR_GRACE_PERIOD } from '@/lib/squid';
import { useLocalStorage } from './useLocalStorage';
@@ -68,8 +68,6 @@ const useLocalNotificationsContext = () => {
[transferNotifications]
);
const squid = useSquid();
useQuery({
queryKey: 'getTransactionStatus',
queryFn: async () => {
@@ -81,23 +79,27 @@ const useLocalNotificationsContext = () => {
toChainId,
fromChainId,
triggeredAt,
isCctp,
errorCount,
status: currentStatus,
} = transferNotification;
// @ts-ignore status.errors is not in the type definition but can be returned
// also error can some time come back as an empty object so we need to ignore for that
const hasErrors = !!currentStatus?.errors ||
(currentStatus?.error && Object.keys(currentStatus.error).length !== 0);
if (
// @ts-ignore status.errors is not in the type definition but can be returned
!currentStatus?.errors &&
!currentStatus?.error &&
!hasErrors &&
(!currentStatus?.squidTransactionStatus ||
currentStatus?.squidTransactionStatus === 'ongoing')
) {
try {
const status = await squid?.getStatus({
const status = await fetchSquidStatus({
transactionId: txHash,
toChainId,
fromChainId,
});
}, isCctp);
if (status) {
transferNotification.status = status;
+11 -2
View File
@@ -1,7 +1,14 @@
import { useMemo } from 'react';
import { useSelector, shallowEqual } from 'react-redux';
import { MarketFilters, MARKET_FILTER_LABELS, type MarketData } from '@/constants/markets';
import {
MarketFilters,
MARKET_FILTER_LABELS,
type MarketData,
MARKETS_TO_DISPLAY,
} from '@/constants/markets';
import { testFlags } from '@/lib/testFlags';
import { getAssets } from '@/state/assetsSelectors';
import { getPerpetualMarkets } from '@/state/perpetualsSelectors';
@@ -42,7 +49,9 @@ export const useMarketsData = (
}, [allPerpetualMarkets, allAssets]);
const filteredMarkets = useMemo(() => {
const filtered = markets.filter(filterFunctions[filter]);
const filtered = markets
.filter(filterFunctions[filter])
.filter(({ id }) => (testFlags.displayAllMarkets ? true : MARKETS_TO_DISPLAY.includes(id)));
if (searchFilter) {
return filtered.filter(
+56 -6
View File
@@ -20,11 +20,13 @@ import {
type NotificationTypeConfig,
NotificationType,
DEFAULT_TOAST_AUTO_CLOSE_MS,
TransferNotificationTypes,
} from '@/constants/notifications';
import { useSelectedNetwork, useStringGetter } from '@/hooks';
import { useLocalNotifications } from '@/hooks/useLocalNotifications';
import { AssetIcon } from '@/components/AssetIcon';
import { Icon, IconName } from '@/components/Icon';
import { TradeNotification } from '@/views/notifications/TradeNotification';
import { TransferStatusNotification } from '@/views/notifications/TransferStatusNotification';
@@ -151,20 +153,20 @@ export const notificationTypes: NotificationTypeConfig[] = [
useEffect(() => {
for (const transfer of transferNotifications) {
const { fromChainId, status, txHash, toAmount } = transfer;
const { fromChainId, status, txHash, toAmount, type } = transfer;
const isFinished = Boolean(status) && status?.squidTransactionStatus !== 'ongoing';
const icon = <Icon iconName={isFinished ? IconName.Transfer : IconName.Clock} />;
const type =
const transferType = type ??
fromChainId === ENVIRONMENT_CONFIG_MAP[selectedNetwork].dydxChainId
? 'withdrawal'
: 'deposit';
? TransferNotificationTypes.Withdrawal
: TransferNotificationTypes.Deposit;
const title = stringGetter({
key: {
deposit: isFinished ? STRING_KEYS.DEPOSIT : STRING_KEYS.DEPOSIT_IN_PROGRESS,
withdrawal: isFinished ? STRING_KEYS.WITHDRAW : STRING_KEYS.WITHDRAW_IN_PROGRESS,
}[type],
}[transferType],
});
const toChainEta = status?.toChain?.chainData?.estimatedRouteDuration || 0;
@@ -189,7 +191,7 @@ export const notificationTypes: NotificationTypeConfig[] = [
slotIcon={icon}
slotTitle={title}
transfer={transfer}
type={type}
type={transferType}
triggeredAt={transfer.triggeredAt}
notification={notification}
/>
@@ -205,9 +207,57 @@ export const notificationTypes: NotificationTypeConfig[] = [
return () => {};
},
},
{
type: NotificationType.ReleaseUpdates,
useTrigger: ({ trigger }) => {
const stringGetter = useStringGetter();
useEffect(() => {
trigger(
'rewards-and-full-trading-live',
{
icon: <AssetIcon symbol="DYDX" />,
title: stringGetter({ key: 'NOTIFICATIONS.RELEASE_REWARDS_AND_FULL_TRADING.TITLE' }),
body: stringGetter({
key: 'NOTIFICATIONS.RELEASE_REWARDS_AND_FULL_TRADING.BODY',
params: {
DOS_BLOGPOST: (
<$Link
href="https://www.dydxopsdao.com/blog/deep-dive-full-trading"
target="_blank"
rel="noopener noreferrer"
>
{stringGetter({ key: STRING_KEYS.HERE })}
</$Link>
),
TRADING_BLOGPOST: (
<$Link
href="https://dydx.exchange/blog/v4-full-trading"
target="_blank"
rel="noopener noreferrer"
>
{stringGetter({ key: STRING_KEYS.HERE })}
</$Link>
),
},
}),
toastSensitivity: 'foreground',
},
[]
);
}, [stringGetter]);
},
useNotificationAction: () => {
return () => {};
},
},
];
const $Icon = styled.img`
height: 1.5rem;
width: 1.5rem;
`;
const $Link = styled.a`
--link-color: var(--color-text-2);
`;
+1
View File
@@ -81,6 +81,7 @@ const useNotificationsContext = () => {
setNotificationPreferences({
[NotificationType.AbacusGenerated]: true,
[NotificationType.SquidTransfer]: true,
[NotificationType.ReleaseUpdates]: true,
version: LOCAL_STORAGE_VERSIONS[LocalStorageKey.NotificationPreferences],
});
}
-50
View File
@@ -1,50 +0,0 @@
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { Squid } from '@0xsquid/sdk';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
import { getSelectedNetwork } from '@/state/appSelectors';
export const NATIVE_TOKEN_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';
export const STATUS_ERROR_GRACE_PERIOD = 300_000;
const useSquidContext = () => {
const selectedNetwork = useSelector(getSelectedNetwork);
const [_, setInitialized] = useState(false);
const initializeClient = async () => {
setInitialized(false);
if (!squid) return;
await squid.init();
setInitialized(true);
};
const squid = useMemo(
() =>
new Squid({
baseUrl: ENVIRONMENT_CONFIG_MAP[selectedNetwork]?.endpoints['0xsquid'],
integratorId: ENVIRONMENT_CONFIG_MAP[selectedNetwork]?.squidIntegratorId,
}),
[selectedNetwork]
);
useEffect(() => {
if (squid) {
initializeClient();
}
}, [squid]);
return squid;
};
type SquidContextType = ReturnType<typeof useSquidContext>;
const SquidContext = createContext<SquidContextType | undefined>(undefined);
SquidContext.displayName = '0xSquid';
export const SquidProvider = ({ ...props }) => (
<SquidContext.Provider value={useSquidContext()} {...props} />
);
export const useSquid = () => useContext(SquidContext);
+21 -3
View File
@@ -31,6 +31,7 @@ import { log } from '@/lib/telemetry';
import { useAccounts } from './useAccounts';
import { useTokenConfigs } from './useTokenConfigs';
import { useDydxClient } from './useDydxClient';
import { hashFromTx } from '@/lib/hashfromTx';
type SubaccountContextType = ReturnType<typeof useSubaccountContext>;
const SubaccountContext = createContext<SubaccountContextType>({} as SubaccountContextType);
@@ -293,12 +294,29 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
);
const sendSquidWithdraw = useCallback(
async (amount: number, payload: string) => {
async (amount: number, payload: string, isCctp?: boolean) => {
const cctpWithdraw = () => {
return new Promise<string>((resolve, reject) =>
abacusStateManager.cctpWithdraw((success, error, data) => {
const parsedData = JSON.parse(data);
if (success && parsedData?.code == 0) {
resolve(parsedData?.transactionHash);
} else {
reject(error);
}
})
)
}
if (isCctp) {
return await cctpWithdraw();
}
if (!subaccountClient) {
return;
}
return await sendSquidWithdrawFromSubaccount({ subaccountClient, amount, payload });
const tx = await sendSquidWithdrawFromSubaccount({ subaccountClient, amount, payload });
return hashFromTx(tx?.hash);
},
[subaccountClient, sendSquidWithdrawFromSubaccount]
);
-46
View File
@@ -1,46 +0,0 @@
import { createContext, useContext, useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import queryString from 'query-string';
class TestFlags {
public queryParams: queryString.ParsedQuery<string>;
constructor() {
this.queryParams = {};
}
setQueryParams(flags: queryString.ParsedQuery<string>) {
this.queryParams = flags;
}
get displayInitializingMarkets() {
return !!this.queryParams.displayInitializingMarkets;
}
}
export const testFlags = new TestFlags();
const useTestFlagsContext = () => {
const [queryParams, setQueryParams] = useState<queryString.ParsedQuery<string>>({});
const location = useLocation();
useEffect(() => {
const parsedQueryParams = queryString.parse(location.search.substring(-1));
testFlags.setQueryParams(parsedQueryParams);
setQueryParams(parsedQueryParams);
}, []);
return {
...queryParams,
};
};
type TestFlagsContextType = ReturnType<typeof useTestFlagsContext>;
const TestFlagsContext = createContext<TestFlagsContextType>({} as TestFlagsContextType);
TestFlagsContext.displayName = 'TestFlags';
export const TestFlagsProvider = ({ ...props }) => (
<TestFlagsContext.Provider value={useTestFlagsContext()} {...props} />
);
export const useTestFlags = () => useContext(TestFlagsContext);
+30
View File
@@ -0,0 +1,30 @@
import { getTradeFormInputs } from '@/state/inputsSelectors';
import { useEffect } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import { TradeInputField } from '@/constants/abacus';
import abacusStateManager from '@/lib/abacus';
export const useTradeFormInputs = () => {
const tradeFormInputValues = useSelector(getTradeFormInputs, shallowEqual);
const { limitPriceInput, triggerPriceInput, trailingPercentInput } = tradeFormInputValues;
useEffect(() => {
abacusStateManager.setTradeValue({
value: triggerPriceInput,
field: TradeInputField.triggerPrice,
});
}, [triggerPriceInput]);
useEffect(() => {
abacusStateManager.setTradeValue({ value: limitPriceInput, field: TradeInputField.limitPrice });
}, [limitPriceInput]);
useEffect(() => {
abacusStateManager.setTradeValue({
value: trailingPercentInput,
field: TradeInputField.trailingPercent,
});
}, [trailingPercentInput]);
};
+1
View File
@@ -32,6 +32,7 @@ export { default as GiftboxIcon } from './giftbox.svg';
export { default as HelpCircleIcon } from './help-circle.svg';
export { default as HideIcon } from './hide.svg';
export { default as HistoryIcon } from './history.svg';
export { default as LeaderboardIcon } from './leaderboard.svg';
export { default as LinkOutIcon } from './link-out.svg';
export { default as LockIcon } from './lock.svg';
export { default as LogoShortIcon } from './logo-short';
+3
View File
@@ -0,0 +1,3 @@
<svg width="13" height="14" viewBox="0 0 13 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.0996094 2.60156C0.0996094 2.07113 0.310323 1.56242 0.685396 1.18735C1.06047 0.812276 1.56918 0.601563 2.09961 0.601562H10.8996C11.43 0.601563 11.9387 0.812276 12.3138 1.18735C12.6889 1.56242 12.8996 2.07113 12.8996 2.60156C12.8996 3.132 12.6889 3.6407 12.3138 4.01578C11.9387 4.39085 11.43 4.60156 10.8996 4.60156H2.09961C1.56918 4.60156 1.06047 4.39085 0.685396 4.01578C0.310323 3.6407 0.0996094 3.132 0.0996094 2.60156ZM0.699609 6.26796C0.540479 6.26796 0.387867 6.33118 0.275345 6.4437C0.162823 6.55622 0.0996094 6.70883 0.0996094 6.86796C0.0996094 7.02709 0.162823 7.1797 0.275345 7.29223C0.387867 7.40475 0.540479 7.46796 0.699609 7.46796H12.2996C12.4587 7.46796 12.6114 7.40475 12.7239 7.29223C12.8364 7.1797 12.8996 7.02709 12.8996 6.86796C12.8996 6.70883 12.8364 6.55622 12.7239 6.4437C12.6114 6.33118 12.4587 6.26796 12.2996 6.26796H0.699609ZM0.699609 9.13196C0.540479 9.13196 0.387867 9.19518 0.275345 9.3077C0.162823 9.42022 0.0996094 9.57283 0.0996094 9.73196C0.0996094 9.89109 0.162823 10.0437 0.275345 10.1562C0.387867 10.2687 0.540479 10.332 0.699609 10.332H12.2996C12.4587 10.332 12.6114 10.2687 12.7239 10.1562C12.8364 10.0437 12.8996 9.89109 12.8996 9.73196C12.8996 9.57283 12.8364 9.42022 12.7239 9.3077C12.6114 9.19518 12.4587 9.13196 12.2996 9.13196H0.699609ZM0.699609 12.0016C0.540479 12.0016 0.387867 12.0648 0.275345 12.1773C0.162823 12.2898 0.0996094 12.4424 0.0996094 12.6016C0.0996094 12.7607 0.162823 12.9133 0.275345 13.0258C0.387867 13.1383 0.540479 13.2016 0.699609 13.2016H12.2996C12.4587 13.2016 12.6114 13.1383 12.7239 13.0258C12.8364 12.9133 12.8996 12.7607 12.8996 12.6016C12.8996 12.4424 12.8364 12.2898 12.7239 12.1773C12.6114 12.0648 12.4587 12.0016 12.2996 12.0016H0.699609Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

+2 -2
View File
@@ -11,13 +11,13 @@ import { DepositDialog } from '@/views/dialogs/DepositDialog';
import { DisconnectDialog } from '@/views/dialogs/DisconnectDialog';
import { ExchangeOfflineDialog } from '@/views/dialogs/ExchangeOfflineDialog';
import { HelpDialog } from '@/views/dialogs/HelpDialog';
import { ExternalLinkDialog } from '@/views/dialogs/ExternalLinkDialog';
import { ExternalNavKeplrDialog } from '@/views/dialogs/ExternalNavKeplrDialog';
import { MnemonicExportDialog } from '@/views/dialogs/MnemonicExportDialog';
import { MobileSignInDialog } from '@/views/dialogs/MobileSignInDialog';
import { OnboardingDialog } from '@/views/dialogs/OnboardingDialog';
import { PreferencesDialog } from '@/views/dialogs/PreferencesDialog';
import { RateLimitDialog } from '@/views/dialogs/RateLimitDialog';
import { ReceiveDialog } from '@/views/dialogs/ReceiveDialog';
import { RestrictedGeoDialog } from '@/views/dialogs/RestrictedGeoDialog';
import { TradeDialog } from '@/views/dialogs/TradeDialog';
import { TransferDialog } from '@/views/dialogs/TransferDialog';
@@ -54,13 +54,13 @@ export const DialogManager = () => {
[DialogTypes.FillDetails]: <FillDetailsDialog {...modalProps} />,
[DialogTypes.Help]: <HelpDialog {...modalProps} />,
[DialogTypes.ExternalNavKeplr]: <ExternalNavKeplrDialog {...modalProps} />,
[DialogTypes.ExternalLink]: <ExternalLinkDialog {...modalProps} />,
[DialogTypes.MnemonicExport]: <MnemonicExportDialog {...modalProps} />,
[DialogTypes.MobileSignIn]: <MobileSignInDialog {...modalProps} />,
[DialogTypes.Onboarding]: <OnboardingDialog {...modalProps} />,
[DialogTypes.OrderDetails]: <OrderDetailsDialog {...modalProps} />,
[DialogTypes.Preferences]: <PreferencesDialog {...modalProps} />,
[DialogTypes.RateLimit]: <RateLimitDialog {...modalProps} />,
[DialogTypes.Receive]: <ReceiveDialog {...modalProps} />,
[DialogTypes.RestrictedGeo]: <RestrictedGeoDialog {...modalProps} />,
[DialogTypes.RestrictedWallet]: <RestrictedWalletDialog {...modalProps} />,
[DialogTypes.Trade]: <TradeDialog {...modalProps} />,
+4 -2
View File
@@ -1,7 +1,7 @@
import styled, { type AnyStyledComponent, css } from 'styled-components';
import { AbacusApiStatus } from '@/constants/abacus';
import { ButtonSize } from '@/constants/buttons';
import { ButtonSize, ButtonType } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { ENVIRONMENT_CONFIG_MAP, isDev } from '@/constants/networks';
@@ -55,10 +55,12 @@ export const FooterDesktop = () => {
}
>
<Styled.FooterButton
type={statusPage ? ButtonType.Link : ButtonType.Button}
slotLeft={<Styled.StatusDot exchangeStatus={exchangeStatus} />}
slotRight={statusPage && <LinkOutIcon />}
size={ButtonSize.XSmall}
state={{ isDisabled: !statusPage }}
href={statusPage}
>
{label}
</Styled.FooterButton>
@@ -132,7 +134,7 @@ Styled.StatusDot = styled.div<{ exchangeStatus: ExchangeStatus }>`
Styled.FooterButton = styled(Button)`
--button-height: 1.5rem;
--button-radius: 0.25rem;
--button-backgroundColor: var(--color-layer-2);
--button-backgroundColor: transparent;
--button-border: none;
--button-textColor: var(--color-text-0);
+29 -13
View File
@@ -1,7 +1,6 @@
import styled, { type AnyStyledComponent } from 'styled-components';
import { useDispatch, useSelector } from 'react-redux';
import { ButtonAction, ButtonSize } from '@/constants/buttons';
import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS } from '@/constants/localization';
import { DEFAULT_MARKETID } from '@/constants/markets';
@@ -11,8 +10,8 @@ import { useShouldShowFooter, useStringGetter } from '@/hooks';
import { layoutMixins } from '@/styles/layoutMixins';
import { NavigationMenu } from '@/components/NavigationMenu';
import { Icon } from '@/components/Icon';
import { BellIcon, MarketsIcon, PlayIcon, PortfolioIcon, ProfileIcon, TradeIcon } from '@/icons';
import { Icon, IconName } from '@/components/Icon';
import { BellIcon, MarketsIcon, PortfolioIcon, ProfileIcon } from '@/icons';
import { IconButton } from '@/components/IconButton';
import { calculateCanAccountTrade } from '@/state/accountCalculators';
@@ -42,11 +41,9 @@ export const FooterMobile = () => {
value: 'trade',
label: stringGetter({ key: STRING_KEYS.TRADE }),
slotBefore: (
<Styled.IconButton
iconComponent={TradeIcon}
size={ButtonSize.Large}
action={ButtonAction.Primary}
/>
<Styled.StartIcon>
<Icon iconName={IconName.Trade} />
</Styled.StartIcon>
),
href: `${AppRoute.Trade}/${marketId ?? DEFAULT_MARKETID}`,
}
@@ -54,11 +51,9 @@ export const FooterMobile = () => {
value: 'onboarding',
label: stringGetter({ key: STRING_KEYS.ONBOARDING }),
slotBefore: (
<Styled.IconButton
iconComponent={PlayIcon}
size={ButtonSize.Large}
action={ButtonAction.Primary}
/>
<Styled.StartIcon>
<Icon iconName={IconName.Play} />
</Styled.StartIcon>
),
onClick: () => dispatch(openDialog({ type: DialogTypes.Onboarding })),
},
@@ -168,3 +163,24 @@ Styled.IconButton = styled(IconButton)`
Styled.Icon = styled(Icon)`
font-size: 1.5rem;
`;
Styled.StartIcon = styled.div`
display: inline-flex;
flex-direction: row;
justify-content: center;
align-items: center;
margin-top: -0.25rem;
width: 3.5rem;
height: 3.5rem;
color: var(--color-text-2);
background-color: var(--color-accent);
border: solid var(--border-width) var(--color-border-white);
border-radius: 50%;
svg {
width: 1.5rem;
height: 1.5rem;
}
`;
+16 -5
View File
@@ -1,6 +1,6 @@
import styled, { type AnyStyledComponent } from 'styled-components';
import { Link } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { ButtonShape } from '@/constants/buttons';
import { DialogTypes } from '@/constants/dialogs';
@@ -8,6 +8,10 @@ import { STRING_KEYS } from '@/constants/localization';
import { AppRoute } from '@/constants/routes';
import { LogoShortIcon, BellStrokeIcon } from '@/icons';
import { headerMixins } from '@/styles/headerMixins';
import { layoutMixins } from '@/styles/layoutMixins';
import breakpoints from '@/styles/breakpoints';
import { useTokenConfigs, useStringGetter, useURLConfigs } from '@/hooks';
import { Icon, IconName } from '@/components/Icon';
@@ -21,10 +25,7 @@ import { NotificationsMenu } from '@/views/menus/NotificationsMenu';
import { LanguageSelector } from '@/views/menus/LanguageSelector';
import { openDialog } from '@/state/dialogs';
import { headerMixins } from '@/styles/headerMixins';
import { layoutMixins } from '@/styles/layoutMixins';
import breakpoints from '@/styles/breakpoints';
import { getHasSeenLaunchIncentives } from '@/state/configsSelectors';
export const HeaderDesktop = () => {
const stringGetter = useStringGetter();
@@ -32,6 +33,8 @@ export const HeaderDesktop = () => {
const dispatch = useDispatch();
const { chainTokenLabel } = useTokenConfigs();
const hasSeenLaunchIncentives = useSelector(getHasSeenLaunchIncentives);
const navItems = [
{
group: 'navigation',
@@ -55,6 +58,7 @@ export const HeaderDesktop = () => {
value: chainTokenLabel,
label: chainTokenLabel,
href: `/${chainTokenLabel}`,
slotAfter: !hasSeenLaunchIncentives && <Styled.UnreadIndicator />,
},
{
value: 'MORE',
@@ -238,3 +242,10 @@ Styled.IconButton = styled(IconButton)<{ size?: string }>`
--button-icon-size: 1rem;
--button-padding: 0 0.5em;
`;
Styled.UnreadIndicator = styled.div`
width: 0.4375rem;
height: 0.4375rem;
border-radius: 50%;
background-color: var(--color-accent);
`;
+77 -14
View File
@@ -1,15 +1,17 @@
import { useMemo } from 'react';
import { useMemo, useState } from 'react';
import styled, { css } from 'styled-components';
import { NotificationStatus } from '@/constants/notifications';
import { ButtonSize } from '@/constants/buttons';
import { ButtonShape, ButtonSize } from '@/constants/buttons';
import { useNotifications } from '@/hooks/useNotifications';
import { useBreakpoints } from '@/hooks/useBreakpoints';
import { ChevronLeftIcon } from '@/icons';
import { breakpoints } from '@/styles';
import { Button } from '@/components/Button';
import { Toast } from '@/components/Toast';
import { ToastArea } from '@/components/ToastArea';
import { ToggleButton } from '@/components/ToggleButton';
type StyleProps = {
className?: string;
@@ -18,6 +20,8 @@ type StyleProps = {
const MAX_TOASTS = 10;
export const NotificationsToastArea = ({ className }: StyleProps) => {
const [shouldStackNotifications, setshouldStackNotifications] = useState(true);
const {
notifications,
getKey,
@@ -43,20 +47,37 @@ export const NotificationsToastArea = ({ className }: StyleProps) => {
key: getKey(notification),
displayData: getDisplayData(notification),
}))
.filter(({ displayData }) => displayData)
.filter(
({ displayData, notification }) =>
displayData && notification.status < NotificationStatus.Unseen
)
.slice(-MAX_TOASTS)
);
}, [notifications, getKey, getDisplayData]);
if (isMenuOpen) return null;
const hasMultipleToasts = notificationMap.length > 1;
return (
<StyledToastArea swipeDirection={isMobile ? 'up' : 'right'} className={className}>
{hasMultipleToasts && (
<StyledToggleButton
shape={ButtonShape.Pill}
size={ButtonSize.XSmall}
isPressed={shouldStackNotifications}
onPressedChange={() => setshouldStackNotifications(!shouldStackNotifications)}
>
<ChevronLeftIcon />
</StyledToggleButton>
)}
{notificationMap.map(({ notification, key, displayData }, idx) => (
<StyledToast
key={key}
layer={notificationMap.length - 1 - idx}
isStacked={shouldStackNotifications}
isOpen={notification.status < NotificationStatus.Unseen}
layer={notificationMap.length - 1 - idx}
notification={notification}
slotIcon={displayData.icon}
slotTitle={displayData.title}
@@ -74,7 +95,7 @@ export const NotificationsToastArea = ({ className }: StyleProps) => {
actionAltText={displayData.actionAltText}
duration={displayData.toastDuration ?? Infinity}
sensitivity={displayData.toastSensitivity}
setIsOpen={(isOpen, isClosedFromTimeout) => {
setIsOpen={(isOpen: boolean, isClosedFromTimeout?: boolean) => {
if (!isOpen)
if (isClosedFromTimeout)
// Toast timer expired without user interaction
@@ -97,16 +118,58 @@ const StyledToastArea = styled(ToastArea)`
padding: 0.75rem 0.75rem 0.75rem 0;
mask-image: linear-gradient(to left, transparent, white 0.5rem);
@media ${breakpoints.mobile} {
width: 100%;
position: fixed;
}
`;
const StyledToast = styled(Toast)<{ layer: number }>`
const StyledToast = styled(Toast)<{ isStacked?: boolean; layer: number }>`
// Stacked toast
${({ isStacked, layer }) =>
isStacked
? css`
position: absolute;
left: 0;
top: calc(${layer} * 2px);
right: calc(${layer} * -2px);
`
: css`
margin-bottom: 0.75rem;
`}
`;
const StyledToggleButton = styled(ToggleButton)<{ isPressed: boolean }>`
z-index: 2;
pointer-events: auto;
display: none;
position: absolute;
top: 0;
left: 0;
right: 0;
${({ layer }) => css`
right: calc(${layer} * -2px);
top: calc(${layer} * 2px);
`}
top: 4px;
left: calc(50% - 0.75rem);
--button-width: 2rem;
--button-height: 1rem;
${StyledToastArea}:hover & {
display: flex;
}
> svg {
width: 4px;
margin-top: 1px;
transform: rotate(-90deg);
${({ isPressed }) =>
!isPressed &&
css`
transform: rotate(90deg);
`}
}
@media ${breakpoints.mobile} {
display: flex;
left: calc(50% - 2rem);
--button-width: 4rem;
--button-height: 2rem;
}
`;
+174 -1
View File
@@ -1,7 +1,8 @@
import Abacus, { type Nullable } from '@dydxprotocol/v4-abacus';
import Long from 'long';
import type { IndexedTx } from '@cosmjs/stargate';
import { encodeJson } from '@dydxprotocol/v4-client-js';
import { GAS_MULTIPLIER, encodeJson } from '@dydxprotocol/v4-client-js';
import { EncodeObject } from '@cosmjs/proto-signing';
import {
CompositeClient,
@@ -9,6 +10,7 @@ import {
type LocalWallet,
Network,
NetworkOptimizer,
NobleClient,
SubaccountClient,
ValidatorConfig,
OrderType,
@@ -40,11 +42,14 @@ import { openDialog } from '@/state/dialogs';
import { StatefulOrderError } from '../errors';
import { bytesToBigInt } from '../numbers';
import { log } from '../telemetry';
import { hashFromTx } from '../hashfromTx';
class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
private compositeClient: CompositeClient | undefined;
private nobleClient: NobleClient | undefined;
private store: RootStore | undefined;
private localWallet: LocalWallet | undefined;
private nobleWallet: LocalWallet | undefined;
constructor() {
this.compositeClient = undefined;
@@ -59,6 +64,17 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
this.localWallet = localWallet;
}
setNobleWallet(nobleWallet: LocalWallet) {
try {
this.nobleWallet = nobleWallet;
if (this.nobleClient) {
this.nobleClient.connect(nobleWallet);
}
} catch (e) {
log('DydxChainTransactions/setNobleWallet', e);
}
}
async connectNetwork(
paramsInJson: Nullable<string>,
callback: (p0: Nullable<string>) => void
@@ -70,6 +86,7 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
websocketUrl,
validatorUrl,
chainId,
nobleValidatorUrl,
USDC_DENOM,
USDC_DECIMALS,
USDC_GAS_DENOM,
@@ -101,6 +118,15 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
this.compositeClient = compositeClient;
try {
if (nobleValidatorUrl) {
this.nobleClient = new NobleClient(nobleValidatorUrl);
if (this.nobleWallet) await this.nobleClient.connect(this.nobleWallet);
}
} catch (e) {
log('DydxChainTransactions/connectNetwork/NobleClient', e);
}
// Dispatch custom event to notify other parts of the app that the network has been connected
const customEvent = new CustomEvent('abacus:connectNetwork', {
detail: parsedParams,
@@ -325,6 +351,130 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
}
}
async sendNobleIBC(
params: {
msgTypeUrl: string,
msg: any,
}
): Promise<string> {
if (!this.nobleClient?.isConnected) {
throw new Error('Missing nobleClient or localWallet');
}
try {
const ibcMsg = {
typeUrl: params.msgTypeUrl, // '/ibc.applications.transfer.v1.MsgTransfer',
value: params.msg,
};
const fee = await this.nobleClient.simulateTransaction([ibcMsg]);
// take out fee from amount before sweeping
const amount = parseInt(ibcMsg.value.token.amount, 10) -
Math.floor(parseInt(fee.amount[0].amount, 10) * GAS_MULTIPLIER);
if (amount <= 0) {
throw new Error('noble balance does not cover fees');
}
ibcMsg.value.token.amount = amount.toString();
const tx = await this.nobleClient.send([ibcMsg]);
const parsedTx = this.parseToPrimitives(tx);
return JSON.stringify(parsedTx);
} catch (error) {
log('DydxChainTransactions/sendNobleIBC', error);
return JSON.stringify({
error,
});
}
}
async withdrawToNobleIBC(
params: {
subaccountNumber: number,
amount: string,
ibcPayload: string,
}
): Promise<string> {
if (!this.compositeClient || !this.localWallet) {
throw new Error('Missing compositeClient or localWallet');
}
const { subaccountNumber, amount, ibcPayload } = params ?? {};
const parsedIbcPayload: {
msgTypeUrl: string,
msg: any,
} = ibcPayload ? JSON.parse(ibcPayload) : undefined;
try {
const msg = this.compositeClient.withdrawFromSubaccountMessage(
new SubaccountClient(this.localWallet, subaccountNumber),
parseFloat(amount).toFixed(this.compositeClient.validatorClient.config.denoms.USDC_DECIMALS)
);
const ibcMsg: EncodeObject = {
typeUrl: parsedIbcPayload.msgTypeUrl,
value: parsedIbcPayload.msg,
};
const tx = await this.compositeClient.send(
this.localWallet,
() => Promise.resolve([msg, ibcMsg]),
false
);
return JSON.stringify({
txHash: hashFromTx(tx?.hash)
});
} catch (error) {
log('DydxChainTransactions/withdrawToNobleIBC', error);
return JSON.stringify({
error,
});
}
}
async cctpWithdraw(params: {
typeUrl: string,
value: any,
}): Promise<string> {
if (!this.nobleClient?.isConnected) {
throw new Error('Missing nobleClient or localWallet');
}
try {
const ibcMsg = {
typeUrl: params.typeUrl, // '/circle.cctp.v1.MsgDepositForBurn',
value: params.value,
};
const fee = await this.nobleClient.simulateTransaction([ibcMsg]);
// take out fee from amount before sweeping
const amount = parseInt(ibcMsg.value.amount, 10) -
Math.floor(parseInt(fee.amount[0].amount, 10) * GAS_MULTIPLIER);
if (amount <= 0) {
throw new Error('noble balance does not cover fees');
}
ibcMsg.value.amount = amount.toString();
const tx = await this.nobleClient.send([ibcMsg]);
const parsedTx = this.parseToPrimitives(tx);
return JSON.stringify(parsedTx);
} catch (error) {
log('DydxChainTransactions/cctpWithdraw', error);
return JSON.stringify({
error,
});
}
}
async transaction(
type: TransactionTypes,
paramsInJson: Abacus.Nullable<string>,
@@ -354,6 +504,22 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
callback(result);
break;
}
case TransactionType.SendNobleIBC: {
const result = await this.sendNobleIBC(params);
callback(result);
break;
}
case TransactionType.WithdrawToNobleIBC: {
const result = await this.withdrawToNobleIBC(params);
callback(result);
break;
}
case TransactionType.CctpWithdraw: {
const result = await this.cctpWithdraw(params);
callback(result);
break;
break;
}
default: {
break;
}
@@ -441,6 +607,13 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
const parseDelegations = this.parseToPrimitives(delegations);
callback(JSON.stringify(parseDelegations));
break;
case QueryType.GetNobleBalance:
if (this.nobleClient?.isConnected) {
const nobleBalance = await this.nobleClient.getAccountBalance('uusdc');
const parsedNobleBalance = this.parseToPrimitives(nobleBalance);
callback(JSON.stringify(parsedNobleBalance));
}
break;
default:
break;
}
+29 -16
View File
@@ -26,12 +26,14 @@ import {
} from '@/constants/abacus';
import { DEFAULT_MARKETID } from '@/constants/markets';
import { CURRENT_ABACUS_DEPLOYMENT, type DydxNetwork } from '@/constants/networks';
import { CURRENT_ABACUS_DEPLOYMENT, type DydxNetwork, isMainnet } from '@/constants/networks';
import { CLEARED_SIZE_INPUTS, CLEARED_TRADE_INPUTS } from '@/constants/trade';
import type { RootStore } from '@/state/_store';
import { setTradeFormInputs } from '@/state/inputs';
import { getInputTradeOptions, getTransferInputs } from '@/state/inputsSelectors';
import { getInputTradeOptions } from '@/state/inputsSelectors';
import { getTransferInputs } from '@/state/inputsSelectors';
import { testFlags } from '@/lib/testFlags';
import AbacusRest from './rest';
import AbacusAnalytics from './analytics';
@@ -43,6 +45,7 @@ import AbacusFormatter from './formatter';
import AbacusThreading from './threading';
import AbacusFileSystem from './filesystem';
import { LocaleSeparators } from '../numbers';
class AbacusStateManager {
private store: RootStore | undefined;
private currentMarket: string | undefined;
@@ -80,10 +83,14 @@ class AbacusStateManager {
this.abacusFormatter
);
const appConfigs = AbacusAppConfig.Companion.forWeb;
if (!isMainnet || testFlags.withCCTP)
appConfigs.squidVersion = AbacusAppConfig.SquidVersion.V2;
this.stateManager = new AsyncAbacusStateManager(
'',
CURRENT_ABACUS_DEPLOYMENT,
AbacusAppConfig.Companion.forWeb,
appConfigs,
ioImplementations,
uiImplementations,
// @ts-ignore
@@ -132,6 +139,8 @@ class AbacusStateManager {
this.setTradeValue({ value: null, field: TradeInputField.limitPrice });
}
this.store?.dispatch(setTradeFormInputs(CLEARED_TRADE_INPUTS));
if (shouldResetSize) {
this.setTradeValue({ value: null, field: TradeInputField.size });
this.setTradeValue({ value: null, field: TradeInputField.usdcSize });
@@ -139,6 +148,8 @@ class AbacusStateManager {
if (needsLeverage) {
this.setTradeValue({ value: null, field: TradeInputField.leverage });
}
this.store?.dispatch(setTradeFormInputs(CLEARED_SIZE_INPUTS));
}
};
@@ -175,6 +186,12 @@ class AbacusStateManager {
}
};
setNobleWallet = (nobleWallet?: LocalWallet) => {
if (nobleWallet) {
this.chainTransactions.setNobleWallet(nobleWallet);
}
};
setTransfersSourceAddress = (evmAddress: string) => {
this.stateManager.sourceAddress = evmAddress;
};
@@ -218,18 +235,6 @@ class AbacusStateManager {
this.abacusFormatter.setLocaleSeparators({ group, decimal });
};
setTransferStatus = ({
hash,
fromChainId,
toChainId,
}: {
hash: string;
fromChainId?: string;
toChainId?: string;
}) => {
this.stateManager.transferStatus(hash, fromChainId, toChainId);
};
// ------ Transactions ------ //
placeOrder = (
@@ -257,6 +262,14 @@ class AbacusStateManager {
) => void
) => this.stateManager.cancelOrder(orderId, callback);
cctpWithdraw = (
callback: (
success: boolean,
parsingError: Nullable<ParsingError>,
data: string,
) => void
): void => this.stateManager.commitCCTPWithdraw(callback);
// ------ Utils ------ //
getHistoricalPnlPeriod = (): Nullable<HistoricalPnlPeriods> =>
this.stateManager.historicalPnlPeriod;
+1 -1
View File
@@ -10,7 +10,7 @@ import {
} from '@/constants/websocket';
import { lastSuccessfulWebsocketRequestByOrigin } from '@/hooks/useAnalytics';
import { testFlags } from '@/hooks/useTestFlags';
import { testFlags } from '@/lib/testFlags';
import { subscriptionsByChannelId } from '@/lib/tradingView/dydxfeed/cache';
import { mapCandle } from '@/lib/tradingView/utils';
+3
View File
@@ -0,0 +1,3 @@
export const hashFromTx = (
txHash: string | Uint8Array
): string => `0x${Buffer.from(txHash).toString('hex')}`;
+48
View File
@@ -0,0 +1,48 @@
import { isMainnet } from '@/constants/networks';
import { GetStatus, StatusResponse } from '@0xsquid/sdk';
export const NATIVE_TOKEN_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';
export const STATUS_ERROR_GRACE_PERIOD = 300_000;
const getSquidStatusUrl = (isV2: boolean) => {
if (isV2) {
return isMainnet
? 'https://v2.api.squidrouter.com/v2/status'
: 'https://testnet.v2.api.squidrouter.com/v2/status';
}
return isMainnet
? 'https://api.squidrouter.com/v1/status'
: 'https://testnet.api.squidrouter.com/v1/status';
};
export const fetchSquidStatus = async (
params: GetStatus,
isV2?: boolean,
integratorId?: string
): Promise<StatusResponse> => {
const parsedParams: { [key: string]: string } = {
transactionId: params.transactionId,
fromChainId: String(params.fromChainId),
toChainId: String(params.toChainId),
};
if (isV2) parsedParams.bridgeType = 'cctp';
const url = `${getSquidStatusUrl(!!isV2)}?${new URLSearchParams(parsedParams).toString()}`;
const response = await fetch(url, {
headers: {
"x-integrator-id": integratorId || 'dYdX-api'
},
});
if (!response.ok) {
const error = await response.json();
throw new Error(error);
}
return response.json();
};
export const getNobleChainId = () => {
return isMainnet ? 'noble-1' : 'grand-1';
}
+31
View File
@@ -0,0 +1,31 @@
class TestFlags {
public queryParams: { [key: string]: string };
constructor() {
this.queryParams = {};
const hash = window.location.hash;
const queryIndex = hash.indexOf('?');
if (queryIndex === -1) return
const queryParamsString = hash.substring(queryIndex + 1);
const params = new URLSearchParams(queryParamsString);
for (const [key, value] of params) {
this.queryParams[key] = value;
}
}
get displayInitializingMarkets() {
return !!this.queryParams.displayInitializingMarkets;
}
get displayAllMarkets() {
return !!this.queryParams.displayAllMarkets;
}
get withCCTP() {
return !!this.queryParams.withCCTP;
}
}
export const testFlags = new TestFlags();
+98 -35
View File
@@ -2,12 +2,16 @@ import styled, { AnyStyledComponent, css } from 'styled-components';
import { Link } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { useEnsName } from 'wagmi';
import { useNavigate } from 'react-router-dom';
import { ButtonSize } from '@/constants/buttons';
import { TransferInputField, TransferType } from '@/constants/abacus';
import { layoutMixins } from '@/styles/layoutMixins';
import { Details } from '@/components/Details';
import { FillsTable, FillsTableColumnKey } from '@/views/tables/FillsTable';
import { IconName } from '@/components/Icon';
import { Icon, IconName } from '@/components/Icon';
import { IconButton, type IconButtonProps } from '@/components/IconButton';
import { Panel } from '@/components/Panel';
import { Toolbar } from '@/components/Toolbar';
@@ -16,22 +20,22 @@ import { OnboardingState } from '@/constants/account';
import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS } from '@/constants/localization';
import { AppRoute, PortfolioRoute, HistoryRoute } from '@/constants/routes';
import { wallets, WalletType } from '@/constants/wallets';
import { wallets } from '@/constants/wallets';
import { useAccounts, useStringGetter, useTokenConfigs } from '@/hooks';
import { getOnboardingState } from '@/state/accountSelectors';
import { openDialog } from '@/state/dialogs';
import abacusStateManager from '@/lib/abacus';
import { isTruthy } from '@/lib/isTruthy';
import { truncateAddress } from '@/lib/wallet';
import { layoutMixins } from '@/styles/layoutMixins';
const ENS_CHAIN_ID = 1; // Ethereum
const Profile = () => {
const stringGetter = useStringGetter();
const dispatch = useDispatch();
const navigate = useNavigate();
const onboardingState = useSelector(getOnboardingState);
const isConnected = onboardingState !== OnboardingState.Disconnected;
@@ -46,17 +50,48 @@ const Profile = () => {
const actions = [
{
key: 'settings',
label: stringGetter({ key: STRING_KEYS.SETTINGS }),
icon: { iconName: IconName.Gear },
href: AppRoute.Settings,
},
isConnected && {
key: 'transfers',
label: stringGetter({ key: STRING_KEYS.MANAGE_FUNDS }),
icon: { iconName: IconName.Transfer },
key: 'deposit',
label: stringGetter({ key: STRING_KEYS.DEPOSIT }),
icon: { iconName: IconName.Deposit },
onClick: () => {
dispatch(openDialog({ type: DialogTypes.ManageFunds }));
dispatch(
openDialog({
type: DialogTypes.ManageFunds,
dialogProps: {
selectedTransferType: TransferType.deposit.rawValue,
},
})
);
},
},
{
key: 'withdraw',
label: stringGetter({ key: STRING_KEYS.WITHDRAW }),
icon: { iconName: IconName.Withdraw },
onClick: () => {
dispatch(
openDialog({
type: DialogTypes.ManageFunds,
dialogProps: {
selectedTransferType: TransferType.withdrawal.rawValue,
},
})
);
},
},
{
key: 'transfer',
label: stringGetter({ key: STRING_KEYS.TRANSFER }),
icon: { iconName: IconName.Send },
onClick: () => {
dispatch(
openDialog({
type: DialogTypes.ManageFunds,
dialogProps: {
selectedTransferType: TransferType.transferOut.rawValue,
},
})
);
},
},
isConnected
@@ -69,9 +104,9 @@ const Profile = () => {
},
}
: {
key: 'wallet',
label: stringGetter({ key: STRING_KEYS.WALLET }),
icon: { iconComponent: wallets[walletType || WalletType.OtherWallet].icon },
key: 'connect',
label: stringGetter({ key: STRING_KEYS.CONNECT }),
icon: { iconName: IconName.Transfer },
onClick: () => {
dispatch(openDialog({ type: DialogTypes.Onboarding }));
},
@@ -107,12 +142,7 @@ const Profile = () => {
{actions.map(({ key, label, href, icon, onClick }) => {
const action = (
<>
<Styled.ActionButton
{...icon}
size={ButtonSize.Large}
onClick={onClick}
isCloseIcon={icon.iconName === IconName.Close}
/>
<Styled.ActionButton {...icon} size={ButtonSize.Large} onClick={onClick} />
<span>{label}</span>
</>
);
@@ -125,6 +155,26 @@ const Profile = () => {
);
})}
</Styled.Actions>
<Styled.EqualGrid>
<Styled.PanelButton
slotHeader={
<Styled.InlineRow>
<Icon iconName={IconName.Gear} />
{stringGetter({ key: STRING_KEYS.SETTINGS })}
</Styled.InlineRow>
}
onClick={() => navigate(AppRoute.Settings)}
/>
<Styled.PanelButton
slotHeader={
<Styled.InlineRow>
<Icon iconName={IconName.HelpCircle} />
{stringGetter({ key: STRING_KEYS.HELP })}
</Styled.InlineRow>
}
onClick={() => dispatch(openDialog({ type: DialogTypes.Help }))}
/>
</Styled.EqualGrid>
<Styled.EqualGrid>
<Panel
@@ -183,7 +233,7 @@ Styled.MobileProfileLayout = styled.div`
${layoutMixins.contentContainerPage}
gap: 1rem;
padding: 1.25rem;
padding: 1.25rem 0.9rem;
`;
Styled.Header = styled.header`
@@ -248,19 +298,21 @@ Styled.Actions = styled(Toolbar)`
}
`;
Styled.ActionButton = styled(IconButton)<{ isCloseIcon?: boolean }>`
Styled.ActionButton = styled(IconButton)<{ iconName?: IconName }>`
margin-bottom: 0.5rem;
${({ isCloseIcon }) =>
isCloseIcon &&
css`
--button-textColor: var(--color-negative);
svg {
width: 0.875em;
height: 0.875em;
}
`}
${({ iconName }) =>
iconName === IconName.Close
? css`
--button-textColor: var(--color-negative);
--button-icon-size: 0.75em;
`
: iconName === IconName.Transfer &&
css`
--button-icon-size: 1.25em;
--button-textColor: var(--color-text-2);
--button-backgroundColor: var(--color-accent);
`}
`;
Styled.EqualGrid = styled.div`
@@ -309,3 +361,14 @@ Styled.TablePanel = styled(Panel)`
}
}
`;
Styled.InlineRow = styled.div`
${layoutMixins.inlineRow}
padding: 1rem;
gap: 0.5rem;
`;
Styled.PanelButton = styled(Panel)`
--panel-paddingY: 0
--panel-paddingX:0;
`;
+10 -4
View File
@@ -2,6 +2,7 @@ import type { ElementType } from 'react';
import styled, { AnyStyledComponent } from 'styled-components';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import breakpoints from '@/styles/breakpoints';
import { layoutMixins } from '@/styles/layoutMixins';
import { useAccountBalance, useAccounts, useTokenConfigs, useStringGetter } from '@/hooks';
@@ -39,7 +40,7 @@ export const DYDXBalancePanel = () => {
<AssetIcon symbol={chainTokenLabel} />
{chainTokenLabel}
</Styled.Title>
<Styled.ReceiveAndTransferButtons>
<Styled.ActionButtons>
{!canAccountTrade ? (
<OnboardingTriggerButton size={ButtonSize.Small} />
) : (
@@ -52,7 +53,7 @@ export const DYDXBalancePanel = () => {
{stringGetter({ key: STRING_KEYS.TRANSFER })}
</Button>
)}
</Styled.ReceiveAndTransferButtons>
</Styled.ActionButtons>
</Styled.Header>
}
>
@@ -116,12 +117,17 @@ const Styled: Record<string, AnyStyledComponent> = {};
Styled.Panel = styled(Panel)`
--panel-paddingX: 1.5rem;
@media ${breakpoints.tablet} {
--panel-paddingY: 1.5rem;
--panel-content-paddingY: 1rem;
}
`;
Styled.Header = styled.div`
${layoutMixins.spacedRow}
gap: 1rem;
padding: 1rem 1.5rem 0;
padding: var(--panel-paddingY) var(--panel-paddingX) 0;
`;
Styled.Title = styled.h3`
@@ -134,7 +140,7 @@ Styled.Title = styled.h3`
}
`;
Styled.ReceiveAndTransferButtons = styled(Toolbar)`
Styled.ActionButtons = styled(Toolbar)`
${layoutMixins.inlineRow}
--stickyArea-topHeight: max-content;
gap: 0.5rem;
+296
View File
@@ -0,0 +1,296 @@
import { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { useQuery } from 'react-query';
import styled, { AnyStyledComponent } from 'styled-components';
import { STRING_KEYS } from '@/constants/localization';
import { ButtonAction } from '@/constants/buttons';
import { DialogTypes } from '@/constants/dialogs';
import breakpoints from '@/styles/breakpoints';
import { useAccounts, useBreakpoints, useStringGetter } from '@/hooks';
import { layoutMixins } from '@/styles/layoutMixins';
import { Panel } from '@/components/Panel';
import { Button } from '@/components/Button';
import { Output, OutputType } from '@/components/Output';
import { Icon, IconName } from '@/components/Icon';
import { Tag, TagSize } from '@/components/Tag';
import { markLaunchIncentivesSeen } from '@/state/configs';
import { openDialog } from '@/state/dialogs';
import { log } from '@/lib/telemetry';
export const LaunchIncentivesPanel = () => {
const { isNotTablet } = useBreakpoints();
const dispatch = useDispatch();
useEffect(() => {
dispatch(markLaunchIncentivesSeen());
}, []);
return isNotTablet ? (
<Styled.Panel slotHeader={<LaunchIncentivesTitle />} slotRight={<EstimatedRewards />}>
<LaunchIncentivesContent />
</Styled.Panel>
) : (
<Styled.Panel>
<Styled.Column>
<EstimatedRewards />
<LaunchIncentivesTitle />
<LaunchIncentivesContent />
</Styled.Column>
</Styled.Panel>
);
};
const LaunchIncentivesTitle = () => {
const stringGetter = useStringGetter();
return (
<Styled.Title>
{stringGetter({
key: STRING_KEYS.LAUNCH_INCENTIVES_TITLE,
params: {
FOR_V4: <Styled.ForV4>{stringGetter({ key: STRING_KEYS.FOR_V4 })}</Styled.ForV4>,
},
})}
<Styled.NewTag size={TagSize.Medium}>{stringGetter({ key: STRING_KEYS.NEW })}</Styled.NewTag>
</Styled.Title>
);
};
const EstimatedRewards = () => {
const stringGetter = useStringGetter();
const { dydxAddress } = useAccounts();
const { data, isLoading } = useQuery({
enabled: !!dydxAddress,
queryKey: `launch_incentives_rewards_${dydxAddress ?? ''}`,
queryFn: async () => {
if (!dydxAddress) return undefined;
const resp = await fetch(`https://cloud.chaoslabs.co/query/api/dydx/points/${dydxAddress}`);
return (await resp.json())?.incentivePoints;
},
onError: (error: Error) => log('LaunchIncentives/fetchPoints', error),
});
return (
<Styled.EstimatedRewardsCard>
<Styled.EstimatedRewardsCardContent>
<div>
<span>{stringGetter({ key: STRING_KEYS.ESTIMATED_REWARDS })}</span>
<Styled.Season>
{stringGetter({
key: STRING_KEYS.LAUNCH_INCENTIVES_SEASON_NUM,
params: {
SEASON_NUMBER: 1,
},
})}
</Styled.Season>
</div>
<Styled.Points>
<Output type={OutputType.Number} value={data} isLoading={isLoading} fractionDigits={2} />
{data !== undefined && stringGetter({ key: STRING_KEYS.POINTS })}
</Styled.Points>
</Styled.EstimatedRewardsCardContent>
<Styled.Image src="/rewards-stars.svg" />
</Styled.EstimatedRewardsCard>
);
};
const LaunchIncentivesContent = () => {
const stringGetter = useStringGetter();
const dispatch = useDispatch();
return (
<Styled.Column>
<Styled.Description>
{stringGetter({ key: STRING_KEYS.LAUNCH_INCENTIVES_DESCRIPTION })}{' '}
</Styled.Description>
<Styled.ChaosLabsLogo src="/logos/chaos-labs.svg" />
<Styled.ButtonRow>
<Styled.AboutButton
action={ButtonAction.Base}
onClick={() => {
dispatch(
openDialog({
type: DialogTypes.ExternalLink,
dialogProps: { link: 'https://dydx.exchange/blog/v4-full-trading' },
})
);
}}
slotRight={<Styled.LinkOutIcon iconName={IconName.LinkOut} />}
>
{stringGetter({ key: STRING_KEYS.ABOUT })}
</Styled.AboutButton>
<Styled.Button
action={ButtonAction.Primary}
onClick={() => {
dispatch(
openDialog({
type: DialogTypes.ExternalLink,
dialogProps: { link: 'https://community.chaoslabs.xyz/dydx-v4/risk/leaderboard' },
})
);
}}
slotRight={<Styled.LinkOutIcon iconName={IconName.LinkOut} />}
slotLeft={<Icon iconName={IconName.Leaderboard} />}
>
{stringGetter({ key: STRING_KEYS.LEADERBOARD })}
</Styled.Button>
</Styled.ButtonRow>
</Styled.Column>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Panel = styled(Panel)`
--panel-paddingY: 1rem;
--panel-paddingX: 1.5rem;
background-color: var(--color-layer-4);
width: 100%;
@media ${breakpoints.tablet} {
--panel-paddingY: 1.5rem;
}
`;
Styled.ForV4 = styled.span`
color: var(--color-text-0);
`;
Styled.Title = styled.h3`
${layoutMixins.inlineRow}
font: var(--font-medium-book);
color: var(--color-text-2);
@media ${breakpoints.notTablet} {
padding: var(--panel-paddingY) var(--panel-paddingX) 0;
}
`;
Styled.Description = styled.div`
color: var(--color-text-0);
--link-color: var(--color-text-1);
a {
display: inline;
text-decoration: underline;
text-underline-offset: 0.25rem;
::before {
content: ' ';
}
}
`;
Styled.ButtonRow = styled.div`
${layoutMixins.inlineRow}
gap: 0.75rem;
margin-top: 0.5rem;
a:last-child {
--button-width: 100%;
}
`;
Styled.Button = styled(Button)`
--button-padding: 0 1rem;
`;
Styled.LinkOutIcon = styled(Icon)`
color: var(--color-text-1);
`;
Styled.AboutButton = styled(Styled.Button)`
--button-textColor: var(--color-text-2);
--button-backgroundColor: var(--color-layer-6);
--button-border: solid var(--border-width) var(--color-layer-7);
`;
Styled.Column = styled.div`
${layoutMixins.flexColumn}
gap: 0.5rem;
`;
Styled.EstimatedRewardsCard = styled.div`
${layoutMixins.spacedRow}
padding: 1rem 1.25rem;
min-width: 21.25rem;
height: calc(100% - calc(1.5rem * 2));
margin: 1.5rem;
background-color: var(--color-layer-5);
background-image: url('/dots-background.svg');
background-size: cover;
border-radius: 0.75rem;
border: solid var(--border-width) var(--color-layer-6);
color: var(--color-text-1);
@media ${breakpoints.tablet} {
margin: 0 0 0.5rem;
}
`;
Styled.EstimatedRewardsCardContent = styled.div`
${layoutMixins.flexColumn}
gap: 1rem;
height: 100%;
justify-content: space-between;
div {
${layoutMixins.flexColumn}
gap: 0.15rem;
font: var(--font-medium-book);
:first-child {
color: var(--color-text-2);
}
}
`;
Styled.BackgroundDots = styled.img`
position: absolute;
`;
Styled.Season = styled.span`
font: var(--font-small-book);
color: var(--color-text-1);
`;
Styled.Points = styled.span`
${layoutMixins.inlineRow}
gap: 0.25rem;
font: var(--font-large-book);
color: var(--color-text-0);
output {
color: var(--color-text-2);
}
`;
Styled.Image = styled.img`
position: relative;
float: right;
width: 5.25rem;
height: auto;
`;
Styled.ChaosLabsLogo = styled.img`
height: 1.25rem;
align-self: start;
`;
Styled.NewTag = styled(Tag)`
color: var(--color-accent);
background-color: var(--color-accent-faded);
`;
+19 -1
View File
@@ -7,6 +7,8 @@ import { ButtonAction, ButtonSize, ButtonType } from '@/constants/buttons';
import { useAccountBalance, useBreakpoints, useStringGetter } from '@/hooks';
import { breakpoints } from '@/styles';
import { layoutMixins } from '@/styles/layoutMixins';
import { Details } from '@/components/Details';
@@ -134,13 +136,22 @@ const Styled: Record<string, AnyStyledComponent> = {};
Styled.MigratePanel = styled(Panel)`
--panel-paddingX: 1.5rem;
width: 100%;
background-image: url('/dots-background.svg');
background-position: right;
background-repeat: no-repeat;
@media ${breakpoints.tablet} {
--panel-paddingY: 1.5rem;
--panel-content-paddingY: 1rem;
}
`;
Styled.Title = styled.h3`
font: var(--font-medium-book);
color: var(--color-text-2);
padding: 1rem 1.5rem 0;
padding: var(--panel-paddingY) var(--panel-paddingX) 0;
`;
Styled.MigrateAction = styled.div`
@@ -164,6 +175,13 @@ Styled.Token = styled(Output)`
Styled.Description = styled.div`
color: var(--color-text-0);
--link-color: var(--color-text-1);
a {
display: inline;
::before {
content: ' ';
}
}
`;
Styled.Column = styled.div`
+30 -6
View File
@@ -19,6 +19,7 @@ import { openDialog } from '@/state/dialogs';
import { DYDXBalancePanel } from './DYDXBalancePanel';
import { MigratePanel } from './MigratePanel';
import { LaunchIncentivesPanel } from './LaunchIncentivesPanel';
export const RewardsPage = () => {
const dispatch = useDispatch();
@@ -39,9 +40,20 @@ export const RewardsPage = () => {
return (
<Styled.Page>
{import.meta.env.VITE_V3_TOKEN_ADDRESS && <MigratePanel />}
<Styled.PanelRow>
{isTablet && <DYDXBalancePanel />}
{isTablet ? (
<>
<LaunchIncentivesPanel />
<DYDXBalancePanel />
</>
) : (
<Styled.PanelRowIncentivesAndBalance>
<LaunchIncentivesPanel />
<DYDXBalancePanel />
</Styled.PanelRowIncentivesAndBalance>
)}
<Styled.PanelRow>
<Styled.Panel
slotHeader={<Styled.Title>{stringGetter({ key: STRING_KEYS.GOVERNANCE })}</Styled.Title>}
slotRight={panelArrow}
@@ -67,8 +79,6 @@ export const RewardsPage = () => {
</Link>
</Styled.Description>
</Styled.Panel>
{isNotTablet && <DYDXBalancePanel />}
</Styled.PanelRow>
</Styled.Page>
);
@@ -99,6 +109,11 @@ Styled.Page = styled.div`
Styled.Panel = styled(Panel)`
--panel-paddingX: 1.5rem;
@media ${breakpoints.tablet} {
--panel-paddingY: 1.5rem;
--panel-content-paddingY: 1rem;
}
`;
Styled.Title = styled.h3`
@@ -110,20 +125,29 @@ Styled.Title = styled.h3`
Styled.Description = styled.div`
color: var(--color-text-0);
--link-color: var(--color-text-1);
a {
display: inline;
::before {
content: ' ';
}
}
`;
Styled.PanelRow = styled.div`
${layoutMixins.gridEqualColumns}
gap: 1.5rem;
align-items: flex-start;
@media ${breakpoints.tablet} {
grid-auto-flow: row;
grid-template-columns: 1fr;
}
`;
Styled.PanelRowIncentivesAndBalance = styled(Styled.PanelRow)`
grid-template-columns: 2fr 1fr;
`;
Styled.IconButton = styled(IconButton)`
color: var(--color-text-0);
--color-border: var(--color-layer-6);
+131 -116
View File
@@ -1,8 +1,7 @@
import { useState } from 'react';
import { useMemo, useState } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import styled, { type AnyStyledComponent } from 'styled-components';
import { AbacusOrderStatus } from '@/constants/abacus';
import { STRING_KEYS } from '@/constants/localization';
import { useBreakpoints, useStringGetter } from '@/hooks';
@@ -28,7 +27,6 @@ import {
getCurrentMarketTradeInfoNumbers,
getHasUnseenFillUpdates,
getHasUnseenOrderUpdates,
getLatestOrderStatus,
getTradeInfoNumbers,
} from '@/state/accountSelectors';
@@ -82,125 +80,142 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => {
showCurrentMarket ? numOpenOrders : numTotalOpenOrders
);
const tabItems = [
{
value: InfoSection.Position,
label: stringGetter({
key: showCurrentMarket ? STRING_KEYS.POSITION : STRING_KEYS.POSITIONS,
}),
const tabItems = useMemo(
() => [
{
asChild: true,
value: InfoSection.Position,
label: stringGetter({
key: showCurrentMarket ? STRING_KEYS.POSITION : STRING_KEYS.POSITIONS,
}),
tag: showCurrentMarket ? null : shortenNumberForDisplay(numTotalPositions),
tag: showCurrentMarket ? null : shortenNumberForDisplay(numTotalPositions),
content: showCurrentMarket ? (
<PositionInfo showNarrowVariation={isTablet} />
) : (
<PositionsTable
columnKeys={
isTablet
? [
PositionsTableColumnKey.Details,
PositionsTableColumnKey.IndexEntry,
PositionsTableColumnKey.PnL,
]
: [
PositionsTableColumnKey.Market,
PositionsTableColumnKey.Side,
PositionsTableColumnKey.Size,
PositionsTableColumnKey.Leverage,
PositionsTableColumnKey.LiquidationAndOraclePrice,
PositionsTableColumnKey.UnrealizedPnl,
PositionsTableColumnKey.RealizedPnl,
PositionsTableColumnKey.AverageOpenAndClose,
]
}
onNavigate={() => setView(PanelView.CurrentMarket)}
/>
),
},
{
value: InfoSection.Orders,
label: stringGetter({ key: STRING_KEYS.ORDERS }),
content: showCurrentMarket ? (
<PositionInfo showNarrowVariation={isTablet} />
) : (
<PositionsTable
columnKeys={
isTablet
? [
PositionsTableColumnKey.Details,
PositionsTableColumnKey.IndexEntry,
PositionsTableColumnKey.PnL,
]
: [
PositionsTableColumnKey.Market,
PositionsTableColumnKey.Side,
PositionsTableColumnKey.Size,
PositionsTableColumnKey.Leverage,
PositionsTableColumnKey.LiquidationAndOraclePrice,
PositionsTableColumnKey.UnrealizedPnl,
PositionsTableColumnKey.RealizedPnl,
PositionsTableColumnKey.AverageOpenAndClose,
]
}
onNavigate={() => setView(PanelView.CurrentMarket)}
/>
),
},
{
asChild: true,
value: InfoSection.Orders,
label: stringGetter({ key: STRING_KEYS.ORDERS }),
slotRight: isWaitingForOrderToIndex ? (
<Styled.LoadingSpinner />
) : (
ordersTagNumber && (
<Tag type={TagType.Number} isHighlighted={hasUnseenOrderUpdates}>
{ordersTagNumber}
slotRight: isWaitingForOrderToIndex ? (
<Styled.LoadingSpinner />
) : (
ordersTagNumber && (
<Tag type={TagType.Number} isHighlighted={hasUnseenOrderUpdates}>
{ordersTagNumber}
</Tag>
)
),
content: (
<OrdersTable
currentMarket={showCurrentMarket ? currentMarketId : undefined}
columnKeys={
isTablet
? [OrdersTableColumnKey.StatusFill, OrdersTableColumnKey.PriceType]
: [
!showCurrentMarket && OrdersTableColumnKey.Market,
OrdersTableColumnKey.Status,
OrdersTableColumnKey.Side,
OrdersTableColumnKey.AmountFill,
OrdersTableColumnKey.Price,
OrdersTableColumnKey.Trigger,
OrdersTableColumnKey.GoodTil,
!isAccountViewOnly && OrdersTableColumnKey.Actions,
].filter(isTruthy)
}
/>
),
},
{
asChild: true,
value: InfoSection.Fills,
label: stringGetter({ key: STRING_KEYS.FILLS }),
slotRight: fillsTagNumber && (
<Tag type={TagType.Number} isHighlighted={hasUnseenFillUpdates}>
{fillsTagNumber}
</Tag>
)
),
),
content: (
<OrdersTable
currentMarket={showCurrentMarket ? currentMarketId : undefined}
columnKeys={
isTablet
? [OrdersTableColumnKey.StatusFill, OrdersTableColumnKey.PriceType]
: [
!showCurrentMarket && OrdersTableColumnKey.Market,
OrdersTableColumnKey.Status,
OrdersTableColumnKey.Side,
OrdersTableColumnKey.AmountFill,
OrdersTableColumnKey.Price,
OrdersTableColumnKey.Trigger,
OrdersTableColumnKey.GoodTil,
!isAccountViewOnly && OrdersTableColumnKey.Actions,
].filter(isTruthy)
}
/>
),
},
{
value: InfoSection.Fills,
label: stringGetter({ key: STRING_KEYS.FILLS }),
content: (
<FillsTable
currentMarket={showCurrentMarket ? currentMarketId : undefined}
columnKeys={
isTablet
? [
FillsTableColumnKey.Time,
FillsTableColumnKey.TypeAmount,
FillsTableColumnKey.PriceFee,
]
: [
!showCurrentMarket && FillsTableColumnKey.Market,
FillsTableColumnKey.Time,
FillsTableColumnKey.Type,
FillsTableColumnKey.Side,
FillsTableColumnKey.AmountTag,
FillsTableColumnKey.Price,
FillsTableColumnKey.TotalFee,
FillsTableColumnKey.Liquidity,
].filter(isTruthy)
}
columnWidths={{
[FillsTableColumnKey.TypeAmount]: '100%',
}}
/>
),
},
// TODO - TRCL-1693 - re-enable when funding payments are supported
// {
// value: InfoSection.Payments,
// label: stringGetter({ key: STRING_KEYS.PAYMENTS }),
slotRight: fillsTagNumber && (
<Tag type={TagType.Number} isHighlighted={hasUnseenFillUpdates}>
{fillsTagNumber}
</Tag>
),
content: (
<FillsTable
currentMarket={showCurrentMarket ? currentMarketId : undefined}
columnKeys={
isTablet
? [
FillsTableColumnKey.Time,
FillsTableColumnKey.TypeAmount,
FillsTableColumnKey.PriceFee,
]
: [
!showCurrentMarket && FillsTableColumnKey.Market,
FillsTableColumnKey.Time,
FillsTableColumnKey.Type,
FillsTableColumnKey.Side,
FillsTableColumnKey.AmountTag,
FillsTableColumnKey.Price,
FillsTableColumnKey.TotalFee,
FillsTableColumnKey.Liquidity,
].filter(isTruthy)
}
columnWidths={{
[FillsTableColumnKey.TypeAmount]: '100%',
}}
/>
),
},
// TODO - TRCL-1693 - re-enable when funding payments are supported
// {
// value: InfoSection.Payments,
// label: stringGetter({ key: STRING_KEYS.PAYMENTS }),
// tag: shortenNumberForDisplay(
// showCurrentMarket ? numFundingPayments : numTotalFundingPayments
// ),
// content: (
// <FundingPaymentsTable currentMarket={showCurrentMarket ? currentMarket?.id : undefined} />
// ),
// },
];
// tag: shortenNumberForDisplay(
// showCurrentMarket ? numFundingPayments : numTotalFundingPayments
// ),
// content: (
// <FundingPaymentsTable currentMarket={showCurrentMarket ? currentMarket?.id : undefined} />
// ),
// },
],
[
stringGetter,
currentMarketId,
showCurrentMarket,
isTablet,
isWaitingForOrderToIndex,
isAccountViewOnly,
ordersTagNumber,
fillsTagNumber,
hasUnseenFillUpdates,
hasUnseenOrderUpdates,
]
);
return isTablet ? (
<MobileTabs defaultValue={InfoSection.Position} items={tabItems} withBorders={false} />
+7 -1
View File
@@ -7,7 +7,12 @@ import { layoutMixins } from '@/styles/layoutMixins';
import { TradeLayouts } from '@/constants/layout';
import { useBreakpoints, useCurrentMarketId, usePageTitlePriceUpdates } from '@/hooks';
import {
useBreakpoints,
useCurrentMarketId,
usePageTitlePriceUpdates,
useTradeFormInputs,
} from '@/hooks';
import { calculateCanAccountTrade } from '@/state/accountCalculators';
import { getSelectedTradeLayout } from '@/state/layoutSelectors';
@@ -37,6 +42,7 @@ const TradePage = () => {
const [isHorizontalPanelOpen, setIsHorizontalPanelOpen] = useState(true);
usePageTitlePriceUpdates();
useTradeFormInputs();
return isTablet ? (
<Styled.TradeLayoutMobile>
+10 -1
View File
@@ -17,6 +17,7 @@ export interface ConfigsState {
feeTiers?: kollections.List<FeeTier>;
feeDiscounts?: FeeDiscount[];
network?: NetworkConfigs;
hasSeenLaunchIncentives: boolean;
}
const DOCUMENT_THEME_MAP = {
@@ -43,6 +44,10 @@ const initialState: ConfigsState = {
feeDiscounts: undefined,
feeTiers: undefined,
network: undefined,
hasSeenLaunchIncentives: getLocalStorage({
key: LocalStorageKey.HasSeenLaunchIncentives,
defaultValue: false,
}),
};
changeTheme(initialState.appTheme);
@@ -60,7 +65,11 @@ export const configsSlice = createSlice({
...state,
...action.payload,
}),
markLaunchIncentivesSeen: (state: ConfigsState) => {
setLocalStorage({ key: LocalStorageKey.HasSeenLaunchIncentives, value: true });
state.hasSeenLaunchIncentives = true;
},
},
});
export const { setAppTheme, setConfigs } = configsSlice.actions;
export const { setAppTheme, setConfigs, markLaunchIncentivesSeen } = configsSlice.actions;
+3
View File
@@ -5,3 +5,6 @@ export const getAppTheme = (state: RootState) => state.configs.appTheme;
export const getFeeTiers = (state: RootState) => state.configs.feeTiers?.toArray();
export const getFeeDiscounts = (state: RootState) => state.configs.feeDiscounts;
export const getHasSeenLaunchIncentives = (state: RootState) =>
state.configs.hasSeenLaunchIncentives;
+19 -2
View File
@@ -1,4 +1,5 @@
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
import assign from 'lodash/assign';
import type {
InputError,
@@ -9,9 +10,14 @@ import type {
TransferInputs,
} from '@/constants/abacus';
import { CLEARED_SIZE_INPUTS, CLEARED_TRADE_INPUTS } from '@/constants/trade';
type TradeFormInputs = typeof CLEARED_TRADE_INPUTS & typeof CLEARED_SIZE_INPUTS;
export interface InputsState {
current?: Nullable<string>;
inputErrors?: Nullable<InputError[]>;
tradeFormInputs: TradeFormInputs;
tradeInputs?: Nullable<TradeInputs>;
closePositionInputs?: Nullable<ClosePositionInputs>;
transferInputs?: Nullable<TransferInputs>;
@@ -20,6 +26,10 @@ export interface InputsState {
const initialState: InputsState = {
current: undefined,
inputErrors: undefined,
tradeFormInputs: {
...CLEARED_TRADE_INPUTS,
...CLEARED_SIZE_INPUTS,
},
tradeInputs: undefined,
transferInputs: undefined,
};
@@ -37,10 +47,17 @@ export const inputsSlice = createSlice({
inputErrors: errors?.toArray(),
tradeInputs: trade,
closePositionInputs: closePosition,
transferInputs: transfer,
transferInputs: {
...transfer,
isCctp: !!transfer?.isCctp,
} as Nullable<TransferInputs>,
};
},
setTradeFormInputs: (state, action: PayloadAction<Partial<TradeFormInputs>>) => {
state.tradeFormInputs = assign({}, state.tradeFormInputs, action.payload);
},
},
});
export const { setInputs } = inputsSlice.actions;
export const { setInputs, setTradeFormInputs } = inputsSlice.actions;
+5
View File
@@ -120,3 +120,8 @@ export const useTradeFormData = () => {
shallowEqual
);
};
/**
* @returns Tradeform Input states for display. Abacus inputs should track these values.
*/
export const getTradeFormInputs = (state: RootState) => state.inputs.tradeFormInputs;
+2 -3
View File
@@ -57,9 +57,8 @@ export default (store: any) => (next: any) => async (action: PayloadAction<any>)
} else if (globalThis.navigator?.language) {
const browserLanguageBaseTag = globalThis.navigator.language.split('-')[0].toLowerCase();
const locale = SUPPORTED_BASE_TAGS_LOCALE_MAPPING[
browserLanguageBaseTag
] as SupportedLocales;
const locale = (SUPPORTED_BASE_TAGS_LOCALE_MAPPING[browserLanguageBaseTag] ??
SUPPORTED_BASE_TAGS_LOCALE_MAPPING[SupportedLocales.EN]) as SupportedLocales;
if (locale) {
store.dispatch(setSelectedLocale({ locale, isAutoDetect: true }));
+1
View File
@@ -116,6 +116,7 @@
--color-gradient-positive: hsla(158, 49%, 48%, 0.16);
--color-gradient-negative: hsla(0, 100%, 66%, 0.16);
--color-accent-faded: hsla(var(--theme-classic-hue-purple), 100%, 70%, 0.16);
--color-risk-low: var(--theme-classic-color-green);
--color-risk-medium: var(--theme-classic-color-yellow);
+2
View File
@@ -151,6 +151,8 @@ export const formMixins: Record<
--form-input-paddingY: 0.5rem;
--form-input-paddingX: 1rem;
height: 100%;
label {
--label-textColor: var(--color-text-0);
}
-5
View File
@@ -877,11 +877,6 @@ export const layoutMixins: Record<
}
`,
perspectiveArea: css`
perspective: 100rem;
transform-style: preserve-3d;
`,
absolute: css`
position: absolute;
top: 0;
-1
View File
@@ -63,7 +63,6 @@ Styled.AccountInfoSectionContainer = styled.div<{ showAccountInfo?: boolean }>`
${layoutMixins.column}
height: var(--account-info-section-height);
min-height: var(--account-info-section-height);
overflow-x: clip;
${({ showAccountInfo }) =>
!showAccountInfo &&
@@ -203,7 +203,6 @@ const Styled: Record<string, AnyStyledComponent> = {};
Styled.Stack = styled.div`
${layoutMixins.stack}
${layoutMixins.perspectiveArea}
`;
Styled.CornerButton = styled(Button)`
@@ -293,7 +292,14 @@ Styled.TransferButtons = styled.div`
Styled.ConnectedAccountInfoContainer = styled.div<{ $showHeader?: boolean }>`
${layoutMixins.column}
${layoutMixins.withOuterAndInnerBorders}
@media ${breakpoints.notTablet} {
${layoutMixins.withOuterAndInnerBorders}
}
@media ${breakpoints.tablet} {
${layoutMixins.withInnerBorder}
}
${({ $showHeader }) =>
$showHeader &&
+4 -1
View File
@@ -2,7 +2,9 @@ import styled, { type AnyStyledComponent } from 'styled-components';
import { shallowEqual, useSelector } from 'react-redux';
import { STRING_KEYS } from '@/constants/localization';
import { MARKETS_TO_DISPLAY } from '@/constants/markets';
import { useBreakpoints, useStringGetter } from '@/hooks';
import { testFlags } from '@/lib/testFlags';
import { breakpoints } from '@/styles';
import { layoutMixins } from '@/styles/layoutMixins';
@@ -32,6 +34,7 @@ export const ExchangeBillboards: React.FC<ExchangeBillboardsProps> = ({
Object.values(perpetualMarkets)
.filter(Boolean)
.filter(({ id }) => (testFlags.displayAllMarkets ? true : MARKETS_TO_DISPLAY.includes(id)))
.forEach(({ oraclePrice, perpetual }) => {
const { volume24H, trades24H, openInterest = 0 } = perpetual || {};
volume24HUSDC += volume24H ?? 0;
@@ -60,7 +63,7 @@ export const ExchangeBillboards: React.FC<ExchangeBillboardsProps> = ({
},
{
key: 'trades',
labelKey: STRING_KEYS.TRADES,
labelKey: isTablet ? STRING_KEYS.TRADES_24H : STRING_KEYS.TRADES,
value: totalTrades24H || undefined,
type: isTablet ? OutputType.CompactNumber : OutputType.Number,
subLabelKey: !isTablet && STRING_KEYS.TRADES_LABEL,
-5
View File
@@ -213,13 +213,8 @@ Styled.MarketDetails = styled.div`
`;
Styled.Header = styled.header`
/* max-width: fit-content; */
${layoutMixins.column}
gap: 1.25rem;
position: sticky;
bottom: 3rem;
`;
Styled.WrapRow = styled.div`
+1 -7
View File
@@ -453,13 +453,6 @@ Styled.Actions = styled.footer`
> :last-child {
flex: 2;
}
position: sticky;
bottom: clamp(0.5rem, 7.5%, 2rem);
@media ${breakpoints.tablet} {
bottom: 0;
}
`;
Styled.Output = styled(Output)<{ sign: NumberSign; smallText?: boolean; margin?: string }>`
@@ -514,6 +507,7 @@ Styled.PositionInfo = styled.div`
Styled.DetachedSection = styled(DetachedSection)`
padding: 0 1.5rem;
position: relative;
`;
Styled.DetachedScrollableSection = styled(DetachedScrollableSection)`
+3 -2
View File
@@ -5,7 +5,7 @@ import { createSelector } from 'reselect';
import { TradeInputField } from '@/constants/abacus';
import { TradeTypes } from '@/constants/trade';
import { STRING_KEYS } from '@/constants/localization';
import { STRING_KEYS, StringKey } from '@/constants/localization';
import { useStringGetter } from '@/hooks';
import { layoutMixins } from '@/styles/layoutMixins';
@@ -30,7 +30,8 @@ const useTradeTypeOptions = () => {
const allTradeTypeItems = typeOptions?.toArray()?.map(({ type, stringKey }) => ({
value: type,
label: stringGetter({
key: type === TradeTypes.TAKE_PROFIT ? STRING_KEYS.TAKE_PROFIT_LIMIT : stringKey,
key:
type === TradeTypes.TAKE_PROFIT ? STRING_KEYS.TAKE_PROFIT_LIMIT : (stringKey as StringKey),
}),
}));
-1
View File
@@ -82,7 +82,6 @@ const Styled: Record<string, AnyStyledComponent> = {};
Styled.PriceChart = styled.div<{ isChartReady?: boolean }>`
${layoutMixins.stack}
${layoutMixins.perspectiveArea}
height: 100%;
@@ -1,12 +1,15 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components';
import { TransferInputField, TransferType } from '@/constants/abacus';
import { isMainnet } from '@/constants/networks';
import { layoutMixins } from '@/styles/layoutMixins';
import { DepositForm } from '@/views/forms/AccountManagementForms/DepositForm';
import { TestnetDepositForm } from '@/views/forms/AccountManagementForms/TestnetDepositForm';
import abacusStateManager from '@/lib/abacus';
type ElementProps = {
onDeposit?: () => void;
};
@@ -14,6 +17,20 @@ type ElementProps = {
export const DepositDialogContent = ({ onDeposit }: ElementProps) => {
const [showFaucet, setShowFaucet] = useState(!isMainnet);
useEffect(() => {
abacusStateManager.setTransferValue({
field: TransferInputField.type,
value: TransferType.deposit.rawValue,
});
return () => {
abacusStateManager.setTransferValue({
field: TransferInputField.type,
value: null,
});
};
}, []);
return (
<Styled.Content>
{isMainnet || !showFaucet ? (
+46
View File
@@ -0,0 +1,46 @@
import styled, { type AnyStyledComponent } from 'styled-components';
import { ButtonAction, ButtonType } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { useStringGetter } from '@/hooks';
import { Button } from '@/components/Button';
import { Dialog } from '@/components/Dialog';
import { layoutMixins } from '@/styles/layoutMixins';
type ElementProps = {
link: string;
linkDescription?: string;
setIsOpen: (open: boolean) => void;
};
export const ExternalLinkDialog = ({ setIsOpen, link, linkDescription }: ElementProps) => {
const stringGetter = useStringGetter();
return (
<Dialog
isOpen
setIsOpen={setIsOpen}
title={stringGetter({ key: STRING_KEYS.LEAVING_WEBSITE })}
description={
linkDescription ?? stringGetter({ key: STRING_KEYS.LEAVING_WEBSITE_DESCRIPTION })
}
>
<Styled.Content>
<p>{stringGetter({ key: STRING_KEYS.LEAVING_WEBSITE_DISCLAIMER })}.</p>
<Button type={ButtonType.Link} action={ButtonAction.Primary} href={link}>
{stringGetter({ key: STRING_KEYS.CONTINUE })}
</Button>
</Styled.Content>
</Dialog>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Content = styled.div`
${layoutMixins.flexColumn}
gap: 1rem;
font: var(--font-base-book);
`;
+3 -2
View File
@@ -18,13 +18,14 @@ import abacusStateManager from '@/lib/abacus';
import { DepositDialogContent } from './DepositDialog/DepositDialogContent';
type ElementProps = {
selectedTransferType?: string;
setIsOpen?: (open: boolean) => void;
};
export const ManageFundsDialog = ({ setIsOpen }: ElementProps) => {
export const ManageFundsDialog = ({ setIsOpen, selectedTransferType }: ElementProps) => {
const stringGetter = useStringGetter();
const { type } = useSelector(getTransferInputs, shallowEqual) || {};
const currentType = type?.rawValue ?? TransferType.deposit.rawValue;
const currentType = type?.rawValue ?? selectedTransferType ?? TransferType.deposit.rawValue;
const closeDialog = () => setIsOpen?.(false);
@@ -7,6 +7,7 @@ import { STRING_KEYS } from '@/constants/localization';
import { ButtonAction } from '@/constants/buttons';
import { layoutMixins } from '@/styles/layoutMixins';
import { formMixins } from '@/styles/formMixins';
import { Button } from '@/components/Button';
import { Link } from '@/components/Link';
@@ -54,14 +55,14 @@ export const AcknowledgeTerms = ({ onClose, onContinue }: ElementProps) => {
<li>{stringGetter({ key: STRING_KEYS.TOS_LINE5 })}</li>
</ul>
</Styled.TOS>
<Styled.Row>
<Styled.Footer>
<Styled.Button onClick={onClose} action={ButtonAction.Base}>
{stringGetter({ key: STRING_KEYS.CLOSE })}
</Styled.Button>
<Styled.Button onClick={onAcknowledgement} action={ButtonAction.Primary}>
{stringGetter({ key: STRING_KEYS.I_AGREE })}
</Styled.Button>
</Styled.Row>
</Styled.Footer>
</>
);
};
@@ -87,8 +88,9 @@ Styled.TOS = styled.section`
}
`;
Styled.Row = styled.div`
${layoutMixins.row};
Styled.Footer = styled.div`
${formMixins.footer};
${layoutMixins.row}
gap: 1rem;
`;
@@ -8,7 +8,7 @@ import { STRING_KEYS } from '@/constants/localization';
import { Button } from '@/components/Button';
import { openDialog } from '@/state/dialogs';
import { forceOpenDialog } from '@/state/dialogs';
import { getOnboardingState } from '@/state/accountSelectors';
import { OnboardingState } from '@/constants/account';
@@ -26,7 +26,7 @@ export const OnboardingTriggerButton = ({ size = ButtonSize.Small }: StyleProps)
<Button
action={ButtonAction.Primary}
size={size}
onClick={() => dispatch(openDialog({ type: DialogTypes.Onboarding }))}
onClick={() => dispatch(forceOpenDialog({ type: DialogTypes.Onboarding }))}
>
{
{
+12
View File
@@ -53,6 +53,18 @@ export const usePreferenceMenu = () => {
),
onSelect: () => toggleNotifPreference(NotificationType.SquidTransfer),
},
{
value: NotificationType.ReleaseUpdates,
label: "Release Updates",
slotAfter: (
<Switch
name={NotificationType.ReleaseUpdates}
checked={enabledNotifs[NotificationType.ReleaseUpdates]}
onCheckedChange={(enabled: boolean) => null}
/>
),
onSelect: () => toggleNotifPreference(NotificationType.ReleaseUpdates),
}
],
}),
[stringGetter, enabledNotifs]
-124
View File
@@ -1,124 +0,0 @@
import { useState } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components';
import { STRING_KEYS } from '@/constants/localization';
import { DydxChainAsset } from '@/constants/wallets';
import { formMixins } from '@/styles/formMixins';
import { layoutMixins } from '@/styles/layoutMixins';
import { useAccounts, useTokenConfigs, useStringGetter } from '@/hooks';
import { AssetIcon } from '@/components/AssetIcon';
import { CopyButton } from '@/components/CopyButton';
import { Dialog } from '@/components/Dialog';
import { QrCode } from '@/components/QrCode';
import { SelectItem, SelectMenu } from '@/components/SelectMenu';
import { WithDetailsReceipt } from '@/components/WithDetailsReceipt';
import { truncateAddress } from '@/lib/wallet';
import { OnboardingTriggerButton } from './OnboardingTriggerButton';
type ElementProps = {
selectedAsset?: DydxChainAsset;
setIsOpen: (open: boolean) => void;
};
export const ReceiveDialog = ({ selectedAsset = DydxChainAsset.CHAINTOKEN, setIsOpen }: ElementProps) => {
const stringGetter = useStringGetter();
const { dydxAddress } = useAccounts();
const { chainTokenLabel, usdcLabel } = useTokenConfigs();
const [asset, setAsset] = useState(selectedAsset);
const assetOptions = [
{
value: DydxChainAsset.USDC,
label: (
<Styled.InlineRow>
<AssetIcon symbol="USDC" /> {usdcLabel}
</Styled.InlineRow>
),
},
{
value: DydxChainAsset.CHAINTOKEN,
label: (
<Styled.InlineRow>
{/* TODO: conditionally display the native token assetIcon */}
{/* <AssetIcon symbol="DYDX" /> */}
{chainTokenLabel}
</Styled.InlineRow>
),
},
];
return (
<Styled.Dialog isOpen setIsOpen={setIsOpen} title={stringGetter({ key: STRING_KEYS.RECEIVE })}>
<Styled.Content>
<Styled.SelectMenu
label={stringGetter({ key: STRING_KEYS.ASSET })}
value={asset}
onValueChange={setAsset}
>
{assetOptions.map(({ value, label }) => (
<Styled.SelectItem key={value} value={value} label={label} />
))}
</Styled.SelectMenu>
{!dydxAddress ? (
<OnboardingTriggerButton />
) : (
<>
<Styled.WithDetailsReceipt
side="bottom"
detailItems={[
{
key: 'address',
label: stringGetter({ key: STRING_KEYS.DYDX_CHAIN_ADDRESS }),
value: truncateAddress(dydxAddress),
},
]}
>
<QrCode hasLogo value={dydxAddress!} />
</Styled.WithDetailsReceipt>
<CopyButton value={dydxAddress} />
</>
)}
</Styled.Content>
</Styled.Dialog>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Dialog = styled(Dialog)`
--dialog-content-paddingTop: var(--default-border-width);
--dialog-width: 20rem;
`;
Styled.Content = styled.div`
${formMixins.inputsColumn}
gap: 1.25rem;
`;
Styled.SelectMenu = styled(SelectMenu)`
${formMixins.inputSelectMenu}
--form-input-height: 3.5rem;
`;
Styled.SelectItem = styled(SelectItem)`
${formMixins.inputSelectMenuItem}
`;
Styled.InlineRow = styled.span`
${layoutMixins.inlineRow}
height: 100%;
img {
font-size: 1.1em;
}
`;
Styled.WithDetailsReceipt = styled(WithDetailsReceipt)`
border-radius: 0.75em;
`;
@@ -2,9 +2,10 @@ import { type FormEvent, useCallback, useEffect, useMemo, useState } from 'react
import styled, { type AnyStyledComponent } from 'styled-components';
import { type NumberFormatValues } from 'react-number-format';
import { shallowEqual, useSelector } from 'react-redux';
import { parseUnits } from 'viem';
import { Abi, parseUnits } from 'viem';
import erc20 from '@/abi/erc20.json';
import erc20_usdt from '@/abi/erc20_usdt.json';
import { TransferInputField, TransferInputTokenResource, TransferType } from '@/constants/abacus';
import { AlertType } from '@/constants/alerts';
import { ButtonSize } from '@/constants/buttons';
@@ -17,7 +18,6 @@ import type { EvmAddress } from '@/constants/wallets';
import { useAccounts, useDebounce, useStringGetter, useSelectedNetwork } from '@/hooks';
import { useAccountBalance, CHAIN_DEFAULT_TOKEN_ADDRESS } from '@/hooks/useAccountBalance';
import { useLocalNotifications } from '@/hooks/useLocalNotifications';
import { NATIVE_TOKEN_ADDRESS, useSquid } from '@/hooks/useSquid';
import { layoutMixins } from '@/styles/layoutMixins';
import { formMixins } from '@/styles/formMixins';
@@ -37,6 +37,7 @@ import { getTransferInputs } from '@/state/inputsSelectors';
import abacusStateManager from '@/lib/abacus';
import { MustBigNumber } from '@/lib/numbers';
import { getNobleChainId, NATIVE_TOKEN_ADDRESS } from '@/lib/squid';
import { log } from '@/lib/telemetry';
import { parseWalletError } from '@/lib/wallet';
@@ -68,6 +69,7 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
summary,
errors: routeErrors,
errorMessage: routeErrorMessage,
isCctp,
} = useSelector(getTransferInputs, shallowEqual) || {};
const chainId = chainIdStr ? parseInt(chainIdStr) : undefined;
@@ -83,7 +85,7 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
);
const [fromAmount, setFromAmount] = useState('');
const [slippage, setSlippage] = useState(0.01); // 1% slippage
const [slippage, setSlippage] = useState(isCctp ? 0 : 0.01); // 1% slippage
const debouncedAmount = useDebounce<string>(fromAmount, 500);
// Async Data
@@ -98,6 +100,8 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
const debouncedAmountBN = MustBigNumber(debouncedAmount);
const balanceBN = MustBigNumber(balance);
useEffect(() => setSlippage(isCctp ? 0 : 0.01), [isCctp]);
useEffect(() => {
const hasInvalidInput =
debouncedAmountBN.isNaN() || debouncedAmountBN.lte(0) || debouncedAmountBN.gt(balanceBN);
@@ -191,15 +195,23 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
const sourceAmountBN = parseUnits(debouncedAmount, sourceToken.decimals);
if (sourceAmountBN > (allowance as bigint)) {
const { request } = await publicClientWagmi.simulateContract({
account: evmAddress,
address: sourceToken.address as EvmAddress,
abi: erc20,
functionName: 'approve',
args: [requestPayload.targetAddress as EvmAddress, sourceAmountBN],
});
const simulateApprove = async (abi: Abi) =>
publicClientWagmi.simulateContract({
account: evmAddress,
address: sourceToken.address as EvmAddress,
abi,
functionName: 'approve',
args: [requestPayload.targetAddress as EvmAddress, sourceAmountBN],
});
const approveTx = await signerWagmi.writeContract(request);
let result;
try {
result = await simulateApprove(erc20 as Abi);
} catch (e) {
result = await simulateApprove(erc20_usdt as Abi);
}
const approveTx = await signerWagmi.writeContract(result.request);
await publicClientWagmi.waitForTransactionReceipt({
hash: approveTx,
});
@@ -241,11 +253,11 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
if (txHash) {
addTransferNotification({
txHash: txHash,
toChainId: ENVIRONMENT_CONFIG_MAP[selectedNetwork].dydxChainId,
toChainId: !isCctp ? ENVIRONMENT_CONFIG_MAP[selectedNetwork].dydxChainId : getNobleChainId(),
fromChainId: chainIdStr || undefined,
toAmount: summary?.usdcSize || undefined,
triggeredAt: Date.now(),
notificationStatus: NotificationStatus.Triggered,
isCctp,
});
abacusStateManager.clearTransferInputValues();
setFromAmount('');
@@ -109,6 +109,8 @@ export const DepositButtonAndReceipt = ({
? stringGetter({ key: STRING_KEYS.HIDE_ALL_DETAILS })
: stringGetter({ key: STRING_KEYS.SHOW_ALL_DETAILS });
const totalFees = (summary?.bridgeFee || 0) + (summary?.gasFee || 0);
const submitButtonReceipt = [
{
key: 'equity',
@@ -158,9 +160,7 @@ export const DepositButtonAndReceipt = ({
{
key: 'total-fees',
label: <span>{stringGetter({ key: STRING_KEYS.TOTAL_FEES })}</span>,
value: typeof summary?.bridgeFee === 'number' && typeof summary?.gasFee === 'number' && (
<Output type={OutputType.Fiat} value={summary?.bridgeFee + summary?.gasFee} />
),
value: <Output type={OutputType.Fiat} value={totalFees} />,
subitems: feeSubitems,
},
{
@@ -183,7 +183,12 @@ export const DepositButtonAndReceipt = ({
type={OutputType.Text}
value={stringGetter({
key: STRING_KEYS.X_MINUTES_LOWERCASED,
params: { X: Math.round(summary?.estimatedRouteDuration / 60) },
params: {
X:
summary?.estimatedRouteDuration < 60
? '< 1'
: Math.round(summary?.estimatedRouteDuration / 60),
},
})}
/>
),
@@ -2,14 +2,16 @@ import { useState, type FormEvent, useEffect } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import styled, { type AnyStyledComponent } from 'styled-components';
import { ButtonAction, ButtonType } from '@/constants/buttons';
import { ButtonAction, ButtonSize, ButtonType } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
import { useAccounts, useStringGetter, useSubaccount } from '@/hooks';
import { formMixins } from '@/styles/formMixins';
import { OnboardingTriggerButton } from '@/views/dialogs/OnboardingTriggerButton';
import { Button } from '@/components/Button';
import { calculateCanAccountTrade } from '@/state/accountCalculators';
import { getSubaccount } from '@/state/accountSelectors';
import { getSelectedNetwork } from '@/state/appSelectors';
@@ -26,6 +28,7 @@ export const TestnetDepositForm = ({ onDeposit, onError }: DepositFormProps) =>
const { requestFaucetFunds } = useSubaccount();
const subAccount = useSelector(getSubaccount, shallowEqual);
const selectedNetwork = useSelector(getSelectedNetwork);
const canAccountTrade = useSelector(calculateCanAccountTrade, shallowEqual);
const [isLoading, setIsLoading] = useState(false);
@@ -72,9 +75,13 @@ export const TestnetDepositForm = ({ onDeposit, onError }: DepositFormProps) =>
})}
</p>
<Styled.Footer>
<Button action={ButtonAction.Primary} type={ButtonType.Submit} state={{ isLoading }}>
{stringGetter({ key: STRING_KEYS.DEPOSIT_FUNDS })}
</Button>
{!canAccountTrade ? (
<OnboardingTriggerButton size={ButtonSize.Base} />
) : (
<Button action={ButtonAction.Primary} type={ButtonType.Submit} state={{ isLoading }}>
{stringGetter({ key: STRING_KEYS.DEPOSIT_FUNDS })}
</Button>
)}
</Styled.Footer>
</Styled.Form>
);
@@ -57,7 +57,6 @@ export const TokenSelectMenu = ({ selectedToken, onSelectToken }: ElementProps)
<Tag>{type === TransferType.deposit ? 'USDC' : selectedToken?.symbol}</Tag>
</>
),
tooltip: 'swap',
},
]}
>
@@ -10,7 +10,7 @@ import { AlertType } from '@/constants/alerts';
import { ButtonSize } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
import { NotificationStatus } from '@/constants/notifications';
import { NotificationStatus, TransferNotificationTypes } from '@/constants/notifications';
import { NumberSign } from '@/constants/numbers';
import {
@@ -45,6 +45,7 @@ import { getTransferInputs } from '@/state/inputsSelectors';
import abacusStateManager from '@/lib/abacus';
import { MustBigNumber } from '@/lib/numbers';
import { getNobleChainId } from '@/lib/squid';
import { TokenSelectMenu } from './TokenSelectMenu';
import { WithdrawButtonAndReceipt } from './WithdrawForm/WithdrawButtonAndReceipt';
@@ -58,11 +59,6 @@ export const WithdrawForm = () => {
const { sendSquidWithdraw } = useSubaccount();
const { freeCollateral } = useSelector(getSubaccount, shallowEqual) || {};
// User input
const [withdrawAmount, setWithdrawAmount] = useState('');
const [slippage, setSlippage] = useState(0.01); // 0.1% slippage
const debouncedAmount = useDebounce<string>(withdrawAmount, 500);
const {
requestPayload,
token,
@@ -71,8 +67,15 @@ export const WithdrawForm = () => {
resources,
errors: routeErrors,
errorMessage: routeErrorMessage,
isCctp
} = useSelector(getTransferInputs, shallowEqual) || {};
// User input
const [withdrawAmount, setWithdrawAmount] = useState('');
const [slippage, setSlippage] = useState(isCctp ? 0 : 0.01); // 0.1% slippage
const debouncedAmount = useDebounce<string>(withdrawAmount, 500);
const isValidAddress = toAddress && isAddress(toAddress);
const toToken = useMemo(
@@ -89,6 +92,8 @@ export const WithdrawForm = () => {
[freeCollateral?.current]
);
useEffect(() => setSlippage(isCctp ? 0 : 0.01), [isCctp]);
useEffect(() => {
abacusStateManager.setTransferValue({
field: TransferInputField.type,
@@ -163,16 +168,16 @@ export const WithdrawForm = () => {
})
);
} else {
const txHash = await sendSquidWithdraw(debouncedAmountBN.toNumber(), requestPayload.data);
if (txHash?.hash) {
const hash = `0x${Buffer.from(txHash.hash).toString('hex')}`;
const txHash = await sendSquidWithdraw(debouncedAmountBN.toNumber(), requestPayload.data, isCctp);
if (txHash) {
addTransferNotification({
txHash: hash,
fromChainId: ENVIRONMENT_CONFIG_MAP[selectedNetwork].dydxChainId,
txHash: txHash,
type: TransferNotificationTypes.Withdrawal,
fromChainId: !isCctp ? ENVIRONMENT_CONFIG_MAP[selectedNetwork].dydxChainId : getNobleChainId(),
toChainId: chainIdStr || undefined,
toAmount: debouncedAmountBN.toNumber(),
triggeredAt: Date.now(),
notificationStatus: NotificationStatus.Triggered,
isCctp,
});
abacusStateManager.clearTransferInputValues();
setWithdrawAmount('');
@@ -89,14 +89,14 @@ export const WithdrawButtonAndReceipt = ({
const showSubitemsToggle = showFeeBreakdown
? stringGetter({ key: STRING_KEYS.HIDE_ALL_DETAILS })
: stringGetter({ key: STRING_KEYS.SHOW_ALL_DETAILS });
const totalFees = (summary?.bridgeFee || 0) + (summary?.gasFee || 0);
const submitButtonReceipt = [
{
key: 'total-fees',
label: <span>{stringGetter({ key: STRING_KEYS.TOTAL_FEES })}</span>,
value: typeof summary?.bridgeFee === 'number' && typeof summary?.gasFee === 'number' && (
<Output type={OutputType.Fiat} value={summary?.bridgeFee + summary?.gasFee} />
),
value: <Output type={OutputType.Fiat} value={totalFees} />,
subitems: feeSubitems,
},
{
@@ -165,7 +165,12 @@ export const WithdrawButtonAndReceipt = ({
type={OutputType.Text}
value={stringGetter({
key: STRING_KEYS.X_MINUTES_LOWERCASED,
params: { X: Math.round(summary?.estimatedRouteDuration / 60) },
params: {
X:
summary?.estimatedRouteDuration < 60
? '< 1'
: Math.round(summary?.estimatedRouteDuration / 60),
},
})}
/>
),
+21 -22
View File
@@ -1,6 +1,6 @@
import { type FormEvent, useState, Ref, useCallback } from 'react';
import { type FormEvent, useState, Ref, useCallback, useEffect } from 'react';
import styled, { AnyStyledComponent, css } from 'styled-components';
import { shallowEqual, useSelector } from 'react-redux';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import type { NumberFormatValues, SourceInfo } from 'react-number-format';
import { AlertType } from '@/constants/alerts';
@@ -37,7 +37,8 @@ import { WithTooltip } from '@/components/WithTooltip';
import { Orderbook } from '@/views/tables/Orderbook';
import { getCurrentInput, useTradeFormData } from '@/state/inputsSelectors';
import { setTradeFormInputs } from '@/state/inputs';
import { getCurrentInput, getTradeFormInputs, useTradeFormData } from '@/state/inputsSelectors';
import { getCurrentMarketConfig } from '@/state/perpetualsSelectors';
import abacusStateManager from '@/lib/abacus';
@@ -80,6 +81,7 @@ export const TradeForm = ({
const [placeOrderError, setPlaceOrderError] = useState<string>();
const [showOrderbook, setShowOrderbook] = useState(false);
const dispatch = useDispatch();
const stringGetter = useStringGetter();
const { placeOrder } = useSubaccount();
@@ -98,18 +100,21 @@ export const TradeForm = ({
tradeErrors,
} = useTradeFormData();
const { limitPrice, triggerPrice, trailingPercent } = price || {};
const currentInput = useSelector(getCurrentInput);
const { tickSizeDecimals, stepSizeDecimals } =
useSelector(getCurrentMarketConfig, shallowEqual) || {};
const tradeFormInputValues = useSelector(getTradeFormInputs, shallowEqual);
const { limitPriceInput, triggerPriceInput, trailingPercentInput } = tradeFormInputValues;
const needsAdvancedOptions =
needsGoodUntil || timeInForceOptions || executionOptions || needsPostOnly || needsReduceOnly;
const tradeFormInputs: TradeBoxInputConfig[] = [];
const isInputFilled =
Object.values(price || {}).some((val) => val != null) ||
Object.values(tradeFormInputValues).some((val) => val !== '') ||
Object.values(price || {}).some((val) => !!val) ||
[size?.size, size?.usdcSize, size?.leverage].some((val) => val != null);
const hasInputErrors =
@@ -197,14 +202,11 @@ export const TradeForm = ({
<Tag>USD</Tag>
</>
),
onChange: ({ floatValue }: NumberFormatValues) => {
abacusStateManager.setTradeValue({
value: floatValue,
field: TradeInputField.triggerPrice,
});
onChange: ({ value }: NumberFormatValues) => {
dispatch(setTradeFormInputs({ triggerPriceInput: value }));
},
value: triggerPrice ?? '',
decimals: tickSizeDecimals || USD_DECIMALS,
value: triggerPriceInput ?? '',
decimals: tickSizeDecimals ?? USD_DECIMALS,
});
}
@@ -220,11 +222,11 @@ export const TradeForm = ({
<Tag>USD</Tag>
</>
),
onChange: ({ floatValue }: NumberFormatValues) => {
abacusStateManager.setTradeValue({ value: floatValue, field: TradeInputField.limitPrice });
onChange: ({ value }: NumberFormatValues) => {
dispatch(setTradeFormInputs({ limitPriceInput: value }));
},
value: limitPrice ?? '',
decimals: tickSizeDecimals || USD_DECIMALS,
value: limitPriceInput,
decimals: tickSizeDecimals ?? USD_DECIMALS,
});
}
@@ -237,13 +239,10 @@ export const TradeForm = ({
{stringGetter({ key: STRING_KEYS.TRAILING_PERCENT })}
</WithTooltip>
),
onChange: ({ floatValue }: NumberFormatValues) => {
abacusStateManager.setTradeValue({
value: floatValue,
field: TradeInputField.trailingPercent,
});
onChange: ({ value }: NumberFormatValues) => {
dispatch(setTradeFormInputs({ trailingPercentInput: value }));
},
value: trailingPercent ?? '',
value: trailingPercentInput ?? '',
});
}
+8 -7
View File
@@ -1,10 +1,10 @@
import { type Dispatch, type SetStateAction, useCallback, useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import styled, { AnyStyledComponent, css } from 'styled-components';
import { Root, Thumb, Track } from '@radix-ui/react-slider';
import _ from 'lodash';
import { OrderSide } from '@dydxprotocol/v4-client-js';
import { type Nullable, TradeInputField } from '@/constants/abacus';
import { TradeInputField } from '@/constants/abacus';
import { PositionSide } from '@/constants/trade';
import { MustBigNumber, type BigNumberish } from '@/lib/numbers';
@@ -12,11 +12,11 @@ import abacusStateManager from '@/lib/abacus';
type ElementProps = {
leverage?: BigNumberish | null;
leverageInputValue: Nullable<number>;
leverageInputValue: string;
maxLeverage: BigNumberish | null;
orderSide: OrderSide;
positionSide: PositionSide;
setLeverageInputValue: Dispatch<SetStateAction<Nullable<number>>>;
setLeverageInputValue: (value: string) => void;
};
type StyleProps = { className?: string };
@@ -33,6 +33,7 @@ export const LeverageSlider = ({
const leverageBN = MustBigNumber(leverage);
const maxLeverageBN = MustBigNumber(maxLeverage);
const leverageInputBN = MustBigNumber(leverageInputValue || leverage);
const leverageInputNumber = isNaN(leverageInputBN.toNumber()) ? 0 : leverageInputBN.toNumber();
const sliderConfig = useMemo(
() => ({
@@ -79,12 +80,12 @@ export const LeverageSlider = ({
);
const onSliderDrag = ([newLeverage]: number[]) => {
setLeverageInputValue(newLeverage);
setLeverageInputValue(`${newLeverage}`);
debouncedSetAbacusLeverage(newLeverage);
};
const onValueCommit = ([newLeverage]: number[]) => {
setLeverageInputValue(newLeverage);
setLeverageInputValue(`${newLeverage}`);
// Ensure Abacus is updated with the latest, committed value
debouncedSetAbacusLeverage.cancel();
@@ -102,7 +103,7 @@ export const LeverageSlider = ({
min={min}
max={max}
step={0.1}
value={[Math.min(Math.max(leverageInputBN.toNumber(), min), max)]}
value={[Math.min(Math.max(leverageInputNumber, min), max)]}
onValueChange={onSliderDrag}
onValueCommit={onValueCommit}
>
@@ -1,9 +1,8 @@
import type { Dispatch, SetStateAction } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import styled, { AnyStyledComponent } from 'styled-components';
import { OrderSide } from '@dydxprotocol/v4-client-js';
import { TradeInputField, Nullable } from '@/constants/abacus';
import { TradeInputField } from '@/constants/abacus';
import { ButtonShape } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { LEVERAGE_DECIMALS } from '@/constants/numbers';
@@ -30,8 +29,8 @@ import { getSelectedOrderSide, hasPositionSideChanged } from '@/lib/tradeData';
import { LeverageSlider } from './LeverageSlider';
type ElementProps = {
leverageInputValue: Nullable<number>;
setLeverageInputValue: Dispatch<SetStateAction<Nullable<number>>>;
leverageInputValue: string;
setLeverageInputValue: (value: string) => void;
};
export const MarketLeverageInput = ({
@@ -62,21 +61,7 @@ export const MarketLeverageInput = ({
const leveragePosition = postOrderLeverage ? newPositionSide : currentPositionSide;
const onLeverageInput = ({ value, floatValue }: { value: string; floatValue?: number }) => {
const newLeverage = MustBigNumber(floatValue).toFixed();
if (value === '' || newLeverage === 'NaN' || !floatValue) {
setLeverageInputValue(null);
abacusStateManager.setTradeValue({
value: null,
field: TradeInputField.leverage,
});
} else {
updateLeverage(floatValue);
}
};
const updateLeverage = (newLeverage: string | number) => {
const getSignedLeverage = (newLeverage: string | number) => {
const newLeverageBN = MustBigNumber(newLeverage);
const newLeverageSignedBN =
leveragePosition === PositionSide.Short ||
@@ -84,10 +69,33 @@ export const MarketLeverageInput = ({
? newLeverageBN.abs().negated()
: newLeverageBN.abs();
setLeverageInputValue(newLeverageSignedBN.toNumber());
return newLeverageSignedBN.toFixed(LEVERAGE_DECIMALS);
};
const onLeverageInput = ({
floatValue,
formattedValue,
}: {
floatValue?: number;
formattedValue: string;
}) => {
setLeverageInputValue(formattedValue);
const newLeverage = MustBigNumber(floatValue).toFixed();
abacusStateManager.setTradeValue({
value: newLeverageSignedBN.toFixed(LEVERAGE_DECIMALS),
value:
formattedValue === '' || newLeverage === 'NaN' ? null : getSignedLeverage(formattedValue),
field: TradeInputField.leverage,
});
};
const updateLeverage = (newLeverage: string | number) => {
const newLeverageSigned = getSignedLeverage(newLeverage);
setLeverageInputValue(newLeverageSigned);
abacusStateManager.setTradeValue({
value: newLeverageSigned,
field: TradeInputField.leverage,
});
};
@@ -98,11 +106,11 @@ export const MarketLeverageInput = ({
if (leveragePosition === PositionSide.None) return;
const inputValue = leverageInputValue || currentLeverage;
const newInputValue = MustBigNumber(inputValue).negated().toNumber();
const newInputValue = MustBigNumber(inputValue).negated().toFixed(LEVERAGE_DECIMALS);
setLeverageInputValue(newInputValue);
abacusStateManager.setTradeValue({
value: newInputValue.toFixed(LEVERAGE_DECIMALS),
value: newInputValue,
field: TradeInputField.leverage,
});
};
@@ -130,7 +138,7 @@ export const MarketLeverageInput = ({
>
<Styled.LeverageSlider
leverage={currentLeverage}
leverageInputValue={leverageInputValue}
leverageInputValue={getSignedLeverage(leverageInputValue)}
maxLeverage={maxLeverage}
orderSide={orderSide}
positionSide={currentPositionSide}
@@ -9,6 +9,7 @@ import { TRADE_TYPE_STRINGS, MobilePlaceOrderSteps } from '@/constants/trade';
import { useStringGetter, useTokenConfigs } from '@/hooks';
import { AssetIcon } from '@/components/AssetIcon';
import { Button } from '@/components/Button';
import { Output, OutputType, ShowSign } from '@/components/Output';
import { WithDetailsReceipt } from '@/components/WithDetailsReceipt';
@@ -77,7 +78,11 @@ export const PlaceOrderButtonAndReceipt = ({
},
{
key: 'fee',
label: stringGetter({ key: STRING_KEYS.FEE }),
label: (
<WithTooltip tooltip="fee" side="right">
{stringGetter({ key: STRING_KEYS.FEE })}
</WithTooltip>
),
value: <Output type={OutputType.Fiat} value={fee} useGrouping />,
},
{
@@ -85,7 +90,7 @@ export const PlaceOrderButtonAndReceipt = ({
label: (
<>
{stringGetter({ key: STRING_KEYS.MAXIMUM_REWARDS })}
{/* <AssetIcon symbol="DYDX" /> */}
<AssetIcon symbol={chainTokenLabel} />
</>
),
value: (
+42 -24
View File
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import styled, { AnyStyledComponent } from 'styled-components';
import { TradeInputField } from '@/constants/abacus';
@@ -20,8 +20,13 @@ import { Icon, IconName } from '@/components/Icon';
import { ToggleButton } from '@/components/ToggleButton';
import { getCurrentMarketAssetData } from '@/state/assetsSelectors';
import { setTradeFormInputs } from '@/state/inputs';
import { getInputTradeSizeData, getInputTradeOptions } from '@/state/inputsSelectors';
import {
getInputTradeSizeData,
getInputTradeOptions,
getTradeFormInputs,
} from '@/state/inputsSelectors';
import { getCurrentMarketConfig } from '@/state/perpetualsSelectors';
@@ -31,12 +36,8 @@ import { MustBigNumber } from '@/lib/numbers';
import { MarketLeverageInput } from './MarketLeverageInput';
export const TradeSizeInputs = () => {
const [sizeInputValue, setSizeInputValue] = useState<number | null>();
const [usdcInputValue, setUsdcInputValue] = useState<number | null>();
const [leverageInputValue, setLeverageInputValue] = useState<number | null>();
const [showUSDCInputOnTablet, setShowUSDCInputOnTablet] = useState(false);
const dispatch = useDispatch();
const stringGetter = useStringGetter();
const { isTablet } = useBreakpoints();
@@ -48,40 +49,55 @@ export const TradeSizeInputs = () => {
useSelector(getCurrentMarketConfig, shallowEqual) || {};
const { size, usdcSize, leverage, input: lastEditedInput } = inputTradeSizeData || {};
const { needsLeverage } = currentTradeInputOptions || {};
const decimals = stepSizeDecimals || TOKEN_DECIMALS;
const decimals = stepSizeDecimals ?? TOKEN_DECIMALS;
const { amountInput, usdAmountInput, leverageInput } = useSelector(
getTradeFormInputs,
shallowEqual
);
// Update State variables if their inputs are not being source of calculations
// Or if they have been reset to null
useEffect(() => {
if (lastEditedInput !== TradeSizeInput.Size || size == null) {
// Abacus size already respects step size
// using .toFixed to prevent trailing zeros and exponential notation
setSizeInputValue(size);
dispatch(setTradeFormInputs({ amountInput: size ? size.toString() : '' }));
}
if (lastEditedInput !== TradeSizeInput.Usdc || usdcSize == null) {
setUsdcInputValue(usdcSize);
dispatch(setTradeFormInputs({ usdAmountInput: usdcSize ? usdcSize.toString() : '' }));
}
if (lastEditedInput !== TradeSizeInput.Leverage || leverage == null) {
setLeverageInputValue(leverage);
dispatch(setTradeFormInputs({ leverageInput: leverage ? leverage.toString() : '' }));
}
}, [size, usdcSize, leverage, lastEditedInput]);
const onSizeInput = ({ value, floatValue }: { value: string; floatValue?: number }) => {
setSizeInputValue(floatValue);
const onSizeInput = ({
floatValue,
formattedValue,
}: {
floatValue?: number;
formattedValue: string;
}) => {
dispatch(setTradeFormInputs({ amountInput: formattedValue }));
const newAmount = MustBigNumber(floatValue).toFixed(decimals);
abacusStateManager.setTradeValue({
value: value === '' || newAmount === 'NaN' ? null : newAmount,
value: formattedValue === '' || newAmount === 'NaN' ? null : newAmount,
field: TradeInputField.size,
});
};
const onUSDCInput = ({ value, floatValue }: { value: string; floatValue?: number }) => {
setUsdcInputValue(floatValue);
const newUsdcAmount = MustBigNumber(floatValue).toFixed();
const onUSDCInput = ({
floatValue,
formattedValue,
}: {
floatValue?: number;
formattedValue: string;
}) => {
dispatch(setTradeFormInputs({ usdAmountInput: formattedValue }));
const newUsdcAmount = MustBigNumber(floatValue).toFixed(tickSizeDecimals || USD_DECIMALS);
abacusStateManager.setTradeValue({
value: value === '' || newUsdcAmount === 'NaN' ? null : newUsdcAmount,
value: formattedValue === '' || newUsdcAmount === 'NaN' ? null : newUsdcAmount,
field: TradeInputField.usdcSize,
});
};
@@ -113,7 +129,7 @@ export const TradeSizeInputs = () => {
}
slotRight={isTablet && inputToggleButton}
type={InputType.Number}
value={sizeInputValue || ''}
value={amountInput || ''}
/>
);
@@ -122,7 +138,7 @@ export const TradeSizeInputs = () => {
id="trade-usdc"
onInput={onUSDCInput}
type={InputType.Currency}
value={usdcInputValue || ''}
value={usdAmountInput || ''}
decimals={tickSizeDecimals || USD_DECIMALS}
label={
<>
@@ -153,8 +169,10 @@ export const TradeSizeInputs = () => {
{needsLeverage && (
<MarketLeverageInput
leverageInputValue={leverageInputValue}
setLeverageInputValue={setLeverageInputValue}
leverageInputValue={leverageInput}
setLeverageInputValue={(value: string) =>
dispatch(setTradeFormInputs({ leverageInput: value }))
}
/>
)}
</Styled.Column>
+4
View File
@@ -119,6 +119,10 @@ export const TransferForm = ({
return () => {
abacusStateManager.clearTransferInputValues();
abacusStateManager.setTransferValue({
field: TransferInputField.type,
value: null,
});
};
}, []);
+52 -29
View File
@@ -1,12 +1,12 @@
import { memo } from 'react';
import styled, { AnyStyledComponent } from 'styled-components';
import styled, { AnyStyledComponent, css } from 'styled-components';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import type { Dispatch } from '@reduxjs/toolkit';
import { OnboardingState } from '@/constants/account';
import { ButtonAction, ButtonShape, ButtonSize, ButtonType } from '@/constants/buttons';
import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS, TOOLTIP_STRING_KEYS } from '@/constants/localization';
import { STRING_KEYS, StringGetterFunction, TOOLTIP_STRING_KEYS } from '@/constants/localization';
import { isMainnet } from '@/constants/networks';
import { DydxChainAsset, wallets } from '@/constants/wallets';
@@ -40,7 +40,6 @@ import { layoutMixins } from '@/styles/layoutMixins';
import { headerMixins } from '@/styles/headerMixins';
import { MustBigNumber } from '@/lib/numbers';
export const AccountMenu = () => {
const stringGetter = useStringGetter();
const { mintscanBase } = useURLConfigs();
@@ -91,13 +90,15 @@ export const AccountMenu = () => {
<Styled.Address>{truncateAddress(dydxAddress)}</Styled.Address>
</Styled.Column>
<Styled.CopyButton buttonType="icon" value={dydxAddress} shape={ButtonShape.Square} />
<Styled.IconButton
action={ButtonAction.Base}
href={`${mintscanBase}/account/${dydxAddress}`}
iconName={IconName.LinkOut}
shape={ButtonShape.Square}
type={ButtonType.Link}
/>
<WithTooltip tooltipString={stringGetter({ key: STRING_KEYS.MINTSCAN })}>
<Styled.IconButton
action={ButtonAction.Base}
href={`${mintscanBase}/account/${dydxAddress}`}
iconName={IconName.LinkOut}
shape={ButtonShape.Square}
type={ButtonType.Link}
/>
</WithTooltip>
</Styled.AddressRow>
<Styled.AddressRow>
{walletType && (
@@ -127,6 +128,7 @@ export const AccountMenu = () => {
asset={DydxChainAsset.CHAINTOKEN}
dispatch={dispatch}
hasBalance={nativeTokenBalance.gt(0)}
stringGetter={stringGetter}
/>
</div>
<div>
@@ -148,6 +150,7 @@ export const AccountMenu = () => {
asset={DydxChainAsset.USDC}
dispatch={dispatch}
hasBalance={MustBigNumber(usdcBalance).gt(0)}
stringGetter={stringGetter}
withOnboarding
/>
</div>
@@ -167,6 +170,12 @@ export const AccountMenu = () => {
onSelect: onRecoverKeys,
separator: true,
},
{
value: 'Preferences',
icon: <Icon iconName={IconName.Gear} />,
label: stringGetter({ key: STRING_KEYS.PREFERENCES }),
onSelect: () => dispatch(openDialog({ type: DialogTypes.Preferences })),
},
...(onboardingState === OnboardingState.AccountConnected && hdKey
? [
!isMainnet && {
@@ -184,12 +193,6 @@ export const AccountMenu = () => {
},
].filter(isTruthy)
: []),
{
value: 'Preferences',
icon: <Icon iconName={IconName.Gear} />,
label: stringGetter({ key: STRING_KEYS.PREFERENCES }),
onSelect: () => dispatch(openDialog({ type: DialogTypes.Preferences })),
},
{
value: 'Disconnect',
icon: <Icon iconName={IconName.BoxClose} />,
@@ -217,38 +220,48 @@ const AssetActions = memo(
dispatch,
withOnboarding,
hasBalance,
stringGetter,
}: {
asset: DydxChainAsset;
dispatch: Dispatch;
withOnboarding?: boolean;
hasBalance?: boolean;
stringGetter: StringGetterFunction;
}) => (
<Styled.InlineRow>
{[
withOnboarding && {
dialogType: DialogTypes.Deposit,
iconName: IconName.Deposit,
tooltipStringKey: STRING_KEYS.DEPOSIT,
},
withOnboarding &&
hasBalance && {
dialogType: DialogTypes.Withdraw,
iconName: IconName.Withdraw,
tooltipStringKey: STRING_KEYS.WITHDRAW,
},
withOnboarding && {
dialogType: DialogTypes.Deposit,
iconName: IconName.Deposit,
},
hasBalance && {
dialogType: DialogTypes.Transfer,
dialogProps: { selectedAsset: asset },
iconName: IconName.Send,
tooltipStringKey: STRING_KEYS.TRANSFER,
},
]
.filter(isTruthy)
.map(({ iconName, dialogType, dialogProps }) => (
<Styled.IconButton
key={dialogType}
action={ButtonAction.Base}
shape={ButtonShape.Square}
iconName={iconName}
onClick={() => dispatch(openDialog({ type: dialogType, dialogProps }))}
/>
.map(({ iconName, tooltipStringKey, dialogType, dialogProps }) => (
<Styled.WithTooltip
key={tooltipStringKey}
tooltipString={stringGetter({ key: tooltipStringKey })}
>
<Styled.IconButton
key={dialogType}
action={ButtonAction.Base}
shape={ButtonShape.Square}
iconName={iconName}
onClick={() => dispatch(openDialog({ type: dialogType, dialogProps }))}
/>
</Styled.WithTooltip>
))}
</Styled.InlineRow>
)
@@ -370,12 +383,22 @@ Styled.ConnectToChain = styled(Styled.Column)`
}
`;
Styled.IconButton = styled(IconButton)`
Styled.IconButton = styled(IconButton)<{ iconName: IconName }>`
--button-padding: 0 0.25rem;
--button-border: solid var(--border-width) var(--color-layer-6);
${({ iconName }) =>
[IconName.Withdraw, IconName.Deposit].includes(iconName) &&
css`
--button-icon-size: 1.375em;
`}
`;
Styled.CopyButton = styled(CopyButton)`
--button-padding: 0 0.25rem;
--button-border: solid var(--border-width) var(--color-layer-6);
`;
Styled.WithTooltip = styled(WithTooltip)`
--tooltip-backgroundColor: var(--color-layer-5);
`;
@@ -12,10 +12,11 @@ import { LoadingSpinner } from '@/components/Loading/LoadingSpinner';
import { layoutMixins } from '@/styles/layoutMixins';
import { STRING_KEYS } from '@/constants/localization';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
import { TransferNotificationTypes } from '@/constants/notifications';
type ElementProps = {
status?: StatusResponse;
type: 'withdrawal' | 'deposit';
type: TransferNotificationTypes;
};
type StyleProps = {
@@ -46,11 +47,13 @@ export const TransferStatusSteps = ({ className, status, type }: ElementProps &
{
label: stringGetter({
key:
type === 'deposit' ? STRING_KEYS.INITIATED_DEPOSIT : STRING_KEYS.INITIATED_WITHDRAWAL,
type === TransferNotificationTypes.Deposit
? STRING_KEYS.INITIATED_DEPOSIT
: STRING_KEYS.INITIATED_WITHDRAWAL,
}),
step: TransferStatusStep.FromChain,
link:
type === 'deposit'
link:
type === TransferNotificationTypes.Deposit
? status?.fromChain?.transactionUrl
: routeStatus?.[0]?.chainId === dydxChainId && routeStatus[0].txHash
? `${mintscanTxUrl?.replace('{tx_hash}', routeStatus[0].txHash.replace('0x', ''))}`
@@ -63,14 +66,20 @@ export const TransferStatusSteps = ({ className, status, type }: ElementProps &
},
{
label: stringGetter({
key: type === 'deposit' ? STRING_KEYS.DEPOSIT_TO_CHAIN : STRING_KEYS.WITHDRAW_TO_CHAIN,
key:
type === TransferNotificationTypes.Deposit
? STRING_KEYS.DEPOSIT_TO_CHAIN
: STRING_KEYS.WITHDRAW_TO_CHAIN,
params: {
CHAIN: type === 'deposit' ? 'dYdX' : status?.toChain?.chainData?.chainName,
CHAIN:
type === TransferNotificationTypes.Deposit
? 'dYdX'
: status?.toChain?.chainData?.chainName,
},
}),
step: TransferStatusStep.ToChain,
link:
type === 'withdrawal'
type === TransferNotificationTypes.Withdrawal
? status?.toChain?.transactionUrl
: currentStatus?.chainId === dydxChainId && currentStatus?.txHash
? `${mintscanTxUrl?.replace('{tx_hash}', currentStatus.txHash.replace('0x', ''))}`
@@ -5,7 +5,7 @@ import { useInterval, useStringGetter } from '@/hooks';
import { AlertType } from '@/constants/alerts';
import { STRING_KEYS } from '@/constants/localization';
import { TransferNotifcation } from '@/constants/notifications';
import { TransferNotifcation, TransferNotificationTypes } from '@/constants/notifications';
import { formatSeconds } from '@/lib/timeUtils';
@@ -22,7 +22,7 @@ import { layoutMixins } from '@/styles/layoutMixins';
import { TransferStatusSteps } from './TransferStatusSteps';
type ElementProps = {
type: 'withdrawal' | 'deposit';
type: TransferNotificationTypes;
transfer: TransferNotifcation;
triggeredAt?: number;
};
@@ -44,6 +44,7 @@ export const TransferStatusNotification = ({
// @ts-ignore status.errors is not in the type definition but can be returned
const error = status?.errors?.length ? status?.errors[0] : status?.error;
const hasError = error && Object.keys(error).length !== 0;
const updateSecondsLeft = useCallback(() => {
const fromChainEta = (status?.fromChain?.chainData?.estimatedRouteDuration || 0) * 1000;
@@ -54,7 +55,7 @@ export const TransferStatusNotification = ({
useInterval({ callback: updateSecondsLeft });
const inProgressStatusString =
type === 'deposit'
type === TransferNotificationTypes.Deposit
? secondsLeft > 0
? STRING_KEYS.DEPOSIT_STATUS
: STRING_KEYS.DEPOSIT_STATUS_SHORTLY
@@ -63,7 +64,7 @@ export const TransferStatusNotification = ({
: STRING_KEYS.WITHDRAW_STATUS_SHORTLY;
const statusString =
type === 'deposit'
type === TransferNotificationTypes.Deposit
? status?.squidTransactionStatus === 'success'
? STRING_KEYS.DEPOSIT_COMPLETE
: inProgressStatusString
@@ -87,7 +88,7 @@ export const TransferStatusNotification = ({
},
})}
</Styled.Status>
{error && (
{hasError && (
<AlertMessage type={AlertType.Error}>
{stringGetter({
key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE,
@@ -112,7 +113,7 @@ export const TransferStatusNotification = ({
) : (
<Styled.BridgingStatus>
{content}
{!isToast && status?.squidTransactionStatus !== 'success' && (
{!isToast && status?.squidTransactionStatus !== 'success' && !hasError && (
<Styled.TransferStatusSteps status={status} type={type} />
)}
</Styled.BridgingStatus>
@@ -120,8 +121,7 @@ export const TransferStatusNotification = ({
}
slotAction={
isToast &&
status &&
!error && (
status && (
<Styled.Trigger
isOpen={open}
onClick={(e: MouseEvent) => {
+19 -13
View File
@@ -67,7 +67,8 @@ export const MarketsTable = ({ className }: { className?: string }) => {
<Styled.Output
type={OutputType.Percent}
value={MustBigNumber(priceChange24HPercent).abs()}
isNegative={MustBigNumber(priceChange24H).isNegative()}
isPositive={MustBigNumber(priceChange24HPercent).gt(0)}
isNegative={MustBigNumber(priceChange24HPercent).isNegative()}
/>
</>
)}
@@ -96,19 +97,20 @@ export const MarketsTable = ({ className }: { className?: string }) => {
),
},
{
columnKey: 'priceChange24H',
getCellValue: (row) => row.priceChange24H,
columnKey: 'priceChange24HPercent',
getCellValue: (row) => row.priceChange24HPercent,
label: stringGetter({ key: STRING_KEYS.CHANGE_24H }),
renderCell: ({ priceChange24H, priceChange24HPercent, tickSizeDecimals }) => (
<TableCell stacked>
<Styled.InlineRow>
{!priceChange24H ? (
{!priceChange24HPercent ? (
<Output type={OutputType.Text} value={null} />
) : (
<Styled.Output
type={OutputType.Percent}
value={MustBigNumber(priceChange24HPercent).abs()}
isNegative={MustBigNumber(priceChange24H).isNegative()}
isPositive={MustBigNumber(priceChange24HPercent).gt(0)}
isNegative={MustBigNumber(priceChange24HPercent).isNegative()}
/>
)}
</Styled.InlineRow>
@@ -128,26 +130,25 @@ export const MarketsTable = ({ className }: { className?: string }) => {
<Styled.Output
type={OutputType.SmallPercent}
value={row.nextFundingRate}
isPositive={MustBigNumber(row.nextFundingRate).gt(0)}
isNegative={MustBigNumber(row.nextFundingRate).isNegative()}
/>
),
},
{
columnKey: 'openInterest',
getCellValue: (row) => row.openInterest,
getCellValue: (row) => row.openInterestUSDC,
label: stringGetter({ key: STRING_KEYS.OPEN_INTEREST }),
renderCell: (row) => (
<TableCell stacked>
<Output type={OutputType.Fiat} value={row.openInterestUSDC} />
<Output
fractionDigits={LARGE_TOKEN_DECIMALS}
type={OutputType.Number}
value={row.openInterest}
tag={row.asset?.id}
/>
<Output
type={OutputType.Fiat}
value={MustBigNumber(row.openInterest).times(MustBigNumber(row.oraclePrice))}
/>
</TableCell>
),
},
@@ -155,7 +156,7 @@ export const MarketsTable = ({ className }: { className?: string }) => {
columnKey: 'volume24H',
getCellValue: (row) => row.volume24H,
label: stringGetter({ key: STRING_KEYS.VOLUME_24H }),
renderCell: (row) => <Output type={OutputType.Number} value={row.volume24H} />,
renderCell: (row) => <Output type={OutputType.Fiat} value={row.volume24H} />,
},
{
columnKey: 'trades24H',
@@ -241,6 +242,11 @@ Styled.TabletPriceChange = styled(Styled.InlineRow)`
font: var(--font-small-book);
`;
Styled.Output = styled(Output)<{ isNegative?: boolean }>`
color: ${({ isNegative }) => (isNegative ? `var(--color-negative)` : `var(--color-positive)`)};
Styled.Output = styled(Output)<{ isNegative?: boolean; isPositive?: boolean }>`
color: ${({ isNegative, isPositive }) =>
isNegative
? `var(--color-negative)`
: isPositive
? `var(--color-positive)`
: `var(--color-text-1)`};
`;
+91 -59
View File
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import styled, { type AnyStyledComponent, css, keyframes } from 'styled-components';
import { OrderSide } from '@dydxprotocol/v4-client-js';
@@ -9,13 +9,13 @@ import { STRING_KEYS } from '@/constants/localization';
import { useBreakpoints, useStringGetter } from '@/hooks';
import { calculateCanViewAccount } from '@/state/accountCalculators';
import { setTradeFormInputs } from '@/state/inputs';
import { getSubaccountOpenOrdersBySideAndPrice } from '@/state/accountSelectors';
import { getCurrentMarketAssetData } from '@/state/assetsSelectors';
import { getCurrentMarketConfig, getCurrentMarketOrderbook } from '@/state/perpetualsSelectors';
import { getCurrentInput } from '@/state/inputsSelectors';
import abacusStateManager from '@/lib/abacus';
import { MustBigNumber } from '@/lib/numbers';
import { Details } from '@/components/Details';
@@ -42,7 +42,11 @@ type StyleProps = {
className?: string;
};
type RowData = OrderbookLine & { side: 'bid' | 'ask'; mine?: number };
type RowData = Pick<OrderbookLine, 'depth' | 'offset' | 'price' | 'size'> & {
side: 'bid' | 'ask';
mine?: number;
key: string;
};
const useCalculateOrderbookData = ({ maxRowsPerSide }: { maxRowsPerSide: number }) => {
const orderbook = useSelector(getCurrentMarketOrderbook, shallowEqual);
@@ -53,8 +57,9 @@ const useCalculateOrderbookData = ({ maxRowsPerSide }: { maxRowsPerSide: number
return useMemo(() => {
const asks = (orderbook?.asks?.toArray() ?? [])
.map(
(row: OrderbookLine) =>
(row: OrderbookLine, idx: number) =>
({
key: `ask-${idx}`,
side: 'ask',
mine: openOrdersBySideAndPrice[OrderSide.SELL]?.[row.price]?.size,
...row,
@@ -64,8 +69,9 @@ const useCalculateOrderbookData = ({ maxRowsPerSide }: { maxRowsPerSide: number
const bids = (orderbook?.bids?.toArray() ?? [])
.map(
(row: OrderbookLine) =>
(row: OrderbookLine, idx: number) =>
({
key: `bid-${idx}`,
side: 'bid',
mine: openOrdersBySideAndPrice[OrderSide.BUY]?.[row.price]?.size,
...row,
@@ -97,15 +103,44 @@ const useCalculateOrderbookData = ({ maxRowsPerSide }: { maxRowsPerSide: number
}
const spread =
asks[0] && bids[0] ? MustBigNumber(asks[0]?.price ?? 0).minus(bids[0]?.price ?? 0) : null;
asks[0]?.price && bids[0]?.price ? MustBigNumber(asks[0].price).minus(bids[0].price) : null;
const spreadPercent = orderbook?.spreadPercent;
const histogramRange = Math.max(
Number(bids[bids.length - 1]?.depth),
Number(asks[asks.length - 1]?.depth)
isNaN(Number(bids[bids.length - 1]?.depth)) ? 0 : Number(bids[bids.length - 1]?.depth),
isNaN(Number(asks[asks.length - 1]?.depth)) ? 0 : Number(asks[asks.length - 1]?.depth)
);
// Ensure asks and bids are of length maxRowsPerSide by adding empty rows.
let idx = asks.length - 1;
while (asks.length < maxRowsPerSide) {
idx += 1;
asks.push({
key: `ask-${idx}`,
side: 'ask',
size: 0,
price: 0,
offset: 0,
depth: 0,
});
}
idx = bids.length - 1;
while (bids.length < maxRowsPerSide) {
idx += 1;
bids.push({
key: `bid-${idx}`,
side: 'bid',
size: 0,
price: 0,
offset: 0,
depth: 0,
});
}
return { asks, bids, spread, spreadPercent, histogramRange, hasOrderbook: !!orderbook };
}, [orderbook, openOrdersBySideAndPrice]);
};
@@ -142,31 +177,33 @@ const OrderbookTable = ({
getCellValue: (row: RowData) => row.size,
label: stringGetter({ key: STRING_KEYS.ORDERBOOK_ORDER_SIZE }),
tag: symbol,
renderCell: (row: RowData) => (
<Styled.HistogramOutput
highlightText
type={OutputType.Asset}
value={row.size}
fractionDigits={stepSizeDecimals}
histogramSide={histogramSide === 'left' && 'left'}
useGrouping={false}
/>
),
renderCell: (row: RowData) =>
row.size > 0 && (
<Styled.HistogramOutput
highlightText
type={OutputType.Asset}
value={row.size}
fractionDigits={stepSizeDecimals}
histogramSide={histogramSide === 'left' && 'left'}
useGrouping={false}
/>
),
},
{
columnKey: 'price',
getCellValue: (row: RowData) => row.price,
label: stringGetter({ key: STRING_KEYS.PRICE }),
tag: 'USD',
renderCell: (row: RowData) => (
<OrderbookTradesOutput
highlightText
type={OutputType.Number}
value={row.price}
fractionDigits={tickSizeDecimals}
useGrouping={false}
/>
),
renderCell: (row: RowData) =>
row.price > 0 && (
<OrderbookTradesOutput
highlightText
type={OutputType.Number}
value={row.price}
fractionDigits={tickSizeDecimals}
useGrouping={false}
/>
),
},
{
columnKey: 'subaccount-orders',
@@ -194,12 +231,13 @@ const OrderbookTable = ({
label="Orderbook"
data={data}
columns={columns}
getRowKey={(row: RowData) => `${row.side}-${row.price}`}
getRowKey={(row: RowData) => row.key}
getRowAttributes={(row: RowData) => ({
'data-side': row.side,
style: {
'--histogram-bucket-size': row.size,
'--histogram-bucket-depth': row.depth,
'--tr-pointerEvents': row.price ? 'auto' : 'none',
},
})}
onRowAction={onRowAction}
@@ -208,7 +246,6 @@ const OrderbookTable = ({
// Style props
histogramSide={histogramSide}
hideHeader={hideHeader}
withFocusStickyRows
style={{
'--histogram-range': histogramRange,
}}
@@ -223,11 +260,12 @@ export const Orderbook = ({
maxRowsPerSide = ORDERBOOK_MAX_ROWS_PER_SIDE,
hideHeader = false,
}: ElementProps & StyleProps) => {
const dispatch = useDispatch();
const stringGetter = useStringGetter();
const { isTablet } = useBreakpoints();
const currentInput = useSelector(getCurrentInput);
const { symbol = '' } = useSelector(getCurrentMarketAssetData, shallowEqual) ?? {};
const { id = '' } = useSelector(getCurrentMarketAssetData, shallowEqual) ?? {};
const { stepSizeDecimals, tickSizeDecimals } =
useSelector(getCurrentMarketConfig, shallowEqual) ?? {};
@@ -238,19 +276,10 @@ export const Orderbook = ({
maxRowsPerSide,
});
const [showRowsPerSide, setShowRowsPerSide] = useState(0);
// Make rows visible one by one so avoid jumps in initial scroll position
useEffect(() => {
if (showRowsPerSide < maxRowsPerSide) {
setShowRowsPerSide((showRows) => showRows + 1);
}
}, [showRowsPerSide]);
const data = useMemo(
() =>
[
...bids.slice(0, showRowsPerSide).reverse(),
...bids.reverse(),
{
key: 'spread',
slotCustomRow: (props) => (
@@ -267,25 +296,23 @@ export const Orderbook = ({
</Styled.SpreadTableRow>
),
} as CustomRowConfig,
...asks.slice(0, showRowsPerSide),
...asks,
].reverse(),
[asks, bids, spread, spreadPercent, showRowsPerSide, isTablet]
[asks, bids, spread, spreadPercent, isTablet]
);
const onRowAction = useCallback(
(key: string, row: RowData) => {
if (currentInput === 'trade' && key !== 'spread' && row?.price)
abacusStateManager.setTradeValue({
value: row?.price,
field: TradeInputField.limitPrice,
});
if (currentInput === 'trade' && key !== 'spread' && row?.price) {
dispatch(setTradeFormInputs({ limitPriceInput: row?.price?.toString() }));
}
},
[currentInput]
);
const orderbookTableProps = {
showMineColumn,
symbol,
symbol: id,
stepSizeDecimals,
tickSizeDecimals,
histogramRange,
@@ -327,7 +354,7 @@ export const Orderbook = ({
</Styled.Header>
<Styled.SplitOrderbook>
<OrderbookTable data={asks} histogramSide="right" {...orderbookTableProps} />
<OrderbookTable data={bids} histogramSide="left" {...orderbookTableProps} />
<OrderbookTable data={bids.reverse()} histogramSide="left" {...orderbookTableProps} />
</Styled.SplitOrderbook>
</Styled.HorizontalLayout>
);
@@ -497,6 +524,7 @@ Styled.OrderbookTable = styled(OrderbookTradesTable)<StyleProps>`
tr {
--histogram-bucket-depth: 0;
pointer-events: var(--tr-pointerEvents);
&[data-side='bid'] {
--accent-color: var(--color-positive);
@@ -510,17 +538,21 @@ Styled.OrderbookTable = styled(OrderbookTradesTable)<StyleProps>`
content: '';
}
&:not(:active):is(:focus-visible, :focus-within) {
${orderbookMixins.scrollSnapItem}
z-index: 2;
${({ withFocusStickyRows }) =>
withFocusStickyRows &&
css`
&:not(:active):is(:focus-visible, :focus-within) {
${orderbookMixins.scrollSnapItem}
z-index: 2;
&[data-side='bid'] {
top: calc(var(--stickyArea-totalInsetTop) + var(--orderbook-spreadRowHeight));
}
&[data-side='ask'] {
bottom: calc(var(--stickyArea-totalInsetBottom) + var(--orderbook-spreadRowHeight));
}
}
&[data-side='bid'] {
top: calc(var(--stickyArea-totalInsetTop) + var(--orderbook-spreadRowHeight));
}
&[data-side='ask'] {
bottom: calc(var(--stickyArea-totalInsetBottom) + var(--orderbook-spreadRowHeight));
}
}
`}
}
${Styled.HorizontalLayout} & {
+2 -11
View File
@@ -230,14 +230,7 @@ const getOrdersTableColumnDef = ({
label: `${stringGetter({ key: STRING_KEYS.STATUS })} / ${stringGetter({
key: STRING_KEYS.FILL,
})}`,
renderCell: ({
asset,
createdAtMilliseconds,
size,
status,
totalFilled,
resources,
}) => {
renderCell: ({ asset, createdAtMilliseconds, size, status, totalFilled, resources }) => {
const { statusIconColor, statusStringKey } = getStatusIconInfo({
status,
totalFilled,
@@ -345,9 +338,7 @@ export const OrdersTable = ({
if (hasUnseenOrderUpdates) dispatch(viewedOrders());
}, [hasUnseenOrderUpdates]);
const symbol = currentMarket
? allAssets[allPerpetualMarkets[currentMarket]?.assetId]?.symbol
: null;
const symbol = currentMarket ? allAssets[allPerpetualMarkets[currentMarket]?.assetId]?.id : null;
const ordersData = orders.map((order: SubaccountOrder) =>
getHydratedTradingData({