Compare commits

...
Author SHA1 Message Date
Bill He 80778b365e Do not init nobleClient if nobleValidatorUrl does not exist 2023-12-04 10:46:17 -08: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
78 changed files with 1773 additions and 478 deletions
+3 -3
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.25",
"@dydxprotocol/v4-client-js": "^1.0.0",
"@dydxprotocol/v4-localization": "^1.0.5",
"@dydxprotocol/v4-abacus": "^1.1.4",
"@dydxprotocol/v4-client-js": "^1.0.6",
"@dydxprotocol/v4-localization": "^1.0.17",
"@ethersproject/providers": "^5.7.2",
"@js-joda/core": "^5.5.3",
"@radix-ui/react-checkbox": "^1.0.4",
+12 -12
View File
@@ -27,14 +27,14 @@ dependencies:
specifier: ^0.31.0
version: 0.31.0
'@dydxprotocol/v4-abacus':
specifier: ^1.0.25
version: 1.0.25
specifier: ^1.1.4
version: 1.1.4
'@dydxprotocol/v4-client-js':
specifier: ^1.0.0
version: 1.0.0
specifier: ^1.0.6
version: 1.0.6
'@dydxprotocol/v4-localization':
specifier: ^1.0.5
version: 1.0.5
specifier: ^1.0.17
version: 1.0.17
'@ethersproject/providers':
specifier: ^5.7.2
version: 5.7.2
@@ -988,12 +988,12 @@ packages:
resolution: {integrity: sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA==}
dev: true
/@dydxprotocol/v4-abacus@1.0.25:
resolution: {integrity: sha512-DYueeM4MFeMLoucuzEU100oxM7mUYhlbyGuTkeE2gz8GX4hUZksH6osuh6fo5895cEAS2fmbOQbX+fNrai56jQ==}
/@dydxprotocol/v4-abacus@1.1.4:
resolution: {integrity: sha512-gml6qheFsPShE9p3FmzFeStYM9ZVhgMq0K0+y12V7pObnMKbumkOIISgCzc47idkah0GMNhiqwi9faD5SjOy/A==}
dev: false
/@dydxprotocol/v4-client-js@1.0.0:
resolution: {integrity: sha512-ehfHO+zQy795TcJRwtiawadFHZyh4HnpJNP26hCGsIjLE5q6LLHweQHpK/1N/sXU1PBOuQwc7iaJnFop6gYauQ==}
/@dydxprotocol/v4-client-js@1.0.6:
resolution: {integrity: sha512-xiWH+kbix+zhI6EsAnd+NDvkjBgxWtGwQmvpd0PjljWNYSFgUtNe5M+piDdRbl2nhy6YWbxAGTwwS3K/ih5qSw==}
dependencies:
'@cosmjs/amino': 0.30.1
'@cosmjs/encoding': 0.31.1
@@ -1020,8 +1020,8 @@ packages:
- utf-8-validate
dev: false
/@dydxprotocol/v4-localization@1.0.5:
resolution: {integrity: sha512-zZm3GFp/ORz9YjigQFVBlcWmaxYY+XEdIw8AWN5nEbsuiA2Jay+/OA7lO9+nV1J1p1T+TgLo9iczIHJsAo3IsA==}
/@dydxprotocol/v4-localization@1.0.17:
resolution: {integrity: sha512-fybNpi1ono1IdwFQJTszsHbHmGti2BdhTo1nz9KOmKh3tmXdhWObKb64Ve6/6Fjk6h/9+DeZ5ZG4UbYyYMyhCA==}
dev: false
/@dydxprotocol/v4-proto@0.4.1:
+61
View File
@@ -0,0 +1,61 @@
[
{
"chainId": "42161",
"tokenAddress": "0xff970a61a04b1ca14834a43f5de4533ebddb5cc8",
"name": "Arbitrum"
},
{
"chainId": "43114",
"tokenAddress": "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E",
"name": "Avalanche"
},
{
"chainId": "8453",
"tokenAddress": "0x66627F389ae46D881773B7131139b2411980E09E",
"name": "Base"
},
{
"chainId": "1",
"tokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"name": "Ethereum"
},
{
"chainId": "10",
"tokenAddress": "0x7F5c764cBc14f9669B88837ca1490cCa17c31607",
"name": "OP Mainnet"
},
{
"chainId": "421613",
"tokenAddress": "0xfd064a18f3bf249cf1f87fc203e90d8f650f2d63",
"name": "Arbitrum Goerli"
},
{
"chainId": "43113",
"tokenAddress": "0x5425890298aed601595a70AB815c96711a31Bc65",
"name": "Avalanche Fuji"
},
{
"chainId": "84531",
"tokenAddress": "0xf175520c52418dfe19c8098071a252da48cd1c19",
"name": "Base Goerli"
},
{
"chainId": "5",
"tokenAddress": "0x07865c6E87B9F70255377e024ace6630C1Eaa37F",
"name": "Ethereum Goerli"
},
{
"chainId": "grand-1",
"tokenAddress": "uusdc",
"name": "Noble Testnet"
},
{
"chainId": "420",
"tokenAddress": "0xe05606174bac4a6364b31bd0eca4bf4dd368f8c6",
"name": "OP Goerli"
},
{
"chainId": "103",
"name": "Solana Devnet"
}
]
+23 -7
View File
@@ -71,6 +71,7 @@
"https://validator.v4dev.dydx.exchange"
],
"0xsquid": "https://testnet.api.0xsquid.com",
"nobleValidator": "https://rpc.testnet.noble.strange.love",
"faucet": "https://faucet.v4dev.dydx.exchange"
},
"links": {
@@ -143,7 +144,8 @@
"validators": [
"http://54.92.118.111"
],
"0xsquid": "https://testnet.api.0xsquid.com"
"0xsquid": "https://testnet.api.0xsquid.com",
"nobleValidator": "https://rpc.testnet.noble.strange.love"
},
"links": {
"tos": "https://dydx.exchange/v4-terms",
@@ -215,7 +217,8 @@
"validators": [
"http://validator.v4dev4.dydx.exchange"
],
"0xsquid": "https://testnet.api.0xsquid.com"
"0xsquid": "https://testnet.api.0xsquid.com",
"nobleValidator": "https://rpc.testnet.noble.strange.love"
},
"links": {
"tos": "https://dydx.exchange/v4-terms",
@@ -287,7 +290,8 @@
"validators": [
"http://18.223.78.50"
],
"0xsquid": "https://testnet.api.0xsquid.com"
"0xsquid": "https://testnet.api.0xsquid.com",
"nobleValidator": "https://rpc.testnet.noble.strange.love"
},
"links": {
"tos": "https://dydx.exchange/v4-terms",
@@ -360,7 +364,8 @@
"validators": [
"https://validator.v4staging.dydx.exchange"
],
"0xsquid": "https://testnet.api.squidrouter.com"
"0xsquid": "https://testnet.api.squidrouter.com",
"nobleValidator": "https://rpc.testnet.noble.strange.love"
},
"links": {
"tos": "https://dydx.exchange/v4-terms",
@@ -406,6 +411,7 @@
"dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain",
"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://rpc.testnet.noble.strange.love"
},
"links": {
"tos": "https://dydx.exchange/v4-terms",
@@ -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://rpc.testnet.noble.strange.love"
},
"links": {
"tos": "https://dydx.exchange/v4-terms",
@@ -583,6 +591,7 @@
"https://dydx-rpc.liquify.com/api=8878132/dydx"
],
"0xsquid": "https://testnet.api.squidrouter.com",
"nobleValidator": "https://rpc.testnet.noble.strange.love",
"faucet": "https://faucet.v4testnet.dydx.exchange"
},
"links": {
@@ -657,6 +666,7 @@
"https://validator.v4testnet.dydx.exchange"
],
"0xsquid": "https://testnet.api.squidrouter.com",
"nobleValidator": "https://rpc.testnet.noble.strange.love",
"faucet": "https://faucet.v4testnet.dydx.exchange"
},
"links": {
@@ -732,6 +742,7 @@
"https://dydx-testnet.nodefleet.org"
],
"0xsquid": "https://testnet.api.squidrouter.com",
"nobleValidator": "https://rpc.testnet.noble.strange.love",
"faucet": "https://faucet.v4testnet.dydx.exchange"
},
"links": {
@@ -807,6 +818,7 @@
"https://test-dydx.kingnodes.com"
],
"0xsquid": "https://testnet.api.squidrouter.com",
"nobleValidator": "https://rpc.testnet.noble.strange.love",
"faucet": "https://faucet.v4testnet.dydx.exchange"
},
"links": {
@@ -882,6 +894,7 @@
"https://dydx-rpc.liquify.com/api=8878132/dydx"
],
"0xsquid": "https://testnet.api.squidrouter.com",
"nobleValidator": "https://rpc.testnet.noble.strange.love",
"faucet": "https://faucet.v4testnet.dydx.exchange"
},
"links": {
@@ -957,6 +970,7 @@
"https://dydx-testnet-rpc.polkachu.com/"
],
"0xsquid": "https://testnet.api.squidrouter.com",
"nobleValidator": "https://rpc.testnet.noble.strange.love",
"faucet": "https://faucet.v4testnet.dydx.exchange"
},
"links": {
@@ -1023,6 +1037,7 @@
"https://dydx-testnet-full-rpc.public.blastapi.io/"
],
"0xsquid": "https://testnet.api.squidrouter.com",
"nobleValidator": "https://rpc.testnet.noble.strange.love",
"faucet": "https://faucet.v4testnet.dydx.exchange"
},
"links": {
@@ -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

-2
View File
@@ -22,7 +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';
@@ -130,7 +129,6 @@ const providers = [
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 }}
>
{
+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,
+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 = {
+21
View File
@@ -26,3 +26,24 @@ export enum FundingDirection {
ToShort = 'ToShort',
ToLong = 'ToLong',
}
export const MARKETS_TO_DISPLAY = [
'BTC-USD',
'ETH-USD',
'LINK-USD',
'SOL-USD',
'MATIC-USD',
'ATOM-USD',
'AVAX-USD',
'APE-USD',
'XRP-USD',
'UNI-USD',
'ADA-USD',
'TRX-USD',
'OP-USD',
'MKR-USD',
'DOGE-USD',
'SHIB-USD',
'COMP-USD',
'LDO-USD',
];
+2
View File
@@ -4,6 +4,7 @@ import { StatusResponse } from '@0xsquid/sdk';
export enum NotificationType {
AbacusGenerated = 'AbacusGenerated',
SquidTransfer = 'SquidTransfer',
ReleaseUpdates = 'ReleaseUpdates',
}
export enum NotificationComponentType {}
@@ -128,6 +129,7 @@ export type TransferNotifcation = {
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 '@/hooks/useTestFlags';
import { getAssets } from '@/state/assetsSelectors';
import { getPerpetualMarkets } from '@/state/perpetualsSelectors';
@@ -42,7 +49,9 @@ export const useMarketsData = (
}, [allPerpetualMarkets, allAssets]);
const filteredMarkets = useMemo(() => {
const filtered = markets.filter(filterFunctions[filter]);
const filtered = markets
.filter(filterFunctions[filter])
.filter(({ id }) => (testFlags.displayAllMarkets ? true : MARKETS_TO_DISPLAY.includes(id)));
if (searchFilter) {
return filtered.filter(
+49
View File
@@ -25,6 +25,7 @@ import {
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';
@@ -205,9 +206,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);
+7
View File
@@ -16,6 +16,13 @@ class TestFlags {
get displayInitializingMarkets() {
return !!this.queryParams.displayInitializingMarkets;
}
/**
* @description We are temporarily displaying a subset of ACTIVE markets, this flag will allow you to see all ACTIVE markets
*/
get displayAllMarkets() {
return !!this.queryParams.displayAllMarkets;
}
}
export const testFlags = new TestFlags();
+33
View File
@@ -0,0 +1,33 @@
import { getTradeFormInputs } from '@/state/inputsSelectors';
import { useEffect } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import { TradeInputField } from '@/constants/abacus';
import abacusStateManager from '@/lib/abacus';
export const useTradeFormInputs = () => {
const tradeFormInputValues = useSelector(getTradeFormInputs, shallowEqual);
const { limitPriceInput, triggerPriceInput, trailingPercentInput } = tradeFormInputValues;
useEffect(() => {
const floatValue = parseFloat(triggerPriceInput);
abacusStateManager.setTradeValue({
value: floatValue,
field: TradeInputField.triggerPrice,
});
}, [triggerPriceInput]);
useEffect(() => {
const floatValue = parseFloat(limitPriceInput);
abacusStateManager.setTradeValue({ value: floatValue, field: TradeInputField.limitPrice });
}, [limitPriceInput]);
useEffect(() => {
const floatValue = parseFloat(trailingPercentInput);
abacusStateManager.setTradeValue({
value: floatValue,
field: TradeInputField.trailingPercent,
});
}, [trailingPercentInput]);
};
+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;
}
`;
+68 -1
View File
@@ -1,7 +1,7 @@
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 {
CompositeClient,
@@ -9,6 +9,7 @@ import {
type LocalWallet,
Network,
NetworkOptimizer,
NobleClient,
SubaccountClient,
ValidatorConfig,
OrderType,
@@ -43,8 +44,10 @@ import { log } from '../telemetry';
class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
private compositeClient: CompositeClient | undefined;
private nobleClient: NobleClient | undefined;
private store: RootStore | undefined;
private localWallet: LocalWallet | undefined;
private nobleWallet: LocalWallet | undefined;
constructor() {
this.compositeClient = undefined;
@@ -59,6 +62,13 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
this.localWallet = localWallet;
}
setNobleWallet(nobleWallet: LocalWallet) {
this.nobleWallet = nobleWallet;
if (this.nobleClient) {
this.nobleClient.connect(nobleWallet);
}
}
async connectNetwork(
paramsInJson: Nullable<string>,
callback: (p0: Nullable<string>) => void
@@ -70,6 +80,7 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
websocketUrl,
validatorUrl,
chainId,
nobleValidatorUrl,
USDC_DENOM,
USDC_DECIMALS,
USDC_GAS_DENOM,
@@ -101,6 +112,10 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
this.compositeClient = compositeClient;
if (nobleValidatorUrl) {
this.nobleClient = new NobleClient(nobleValidatorUrl);
if (this.nobleWallet) await this.nobleClient.connect(this.nobleWallet);
}
// Dispatch custom event to notify other parts of the app that the network has been connected
const customEvent = new CustomEvent('abacus:connectNetwork', {
detail: parsedParams,
@@ -325,6 +340,46 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
}
}
async sendNobleIBC(
params: {
msgTypeUrl: string,
msg: any,
}
): Promise<string> {
if (!this.nobleClient?.isConnected) {
throw new Error('Missing nobleClient or localWallet');
}
try {
const ibcMsg = {
typeUrl: params.msgTypeUrl, // '/ibc.applications.transfer.v1.MsgTransfer',
value: params.msg,
};
const fee = await this.nobleClient.simulateTransaction([ibcMsg]);
// take out fee from amount before sweeping
const amount = parseInt(ibcMsg.value.token.amount, 10) -
Math.floor(parseInt(fee.amount[0].amount, 10) * GAS_MULTIPLIER);
if (amount <= 0) {
throw new Error('noble balance does not cover fees');
}
ibcMsg.value.token.amount = amount.toString();
const tx = await this.nobleClient.send([ibcMsg]);
const parsedTx = this.parseToPrimitives(tx);
return JSON.stringify(parsedTx);
} catch (error) {
log('DydxChainTransactions/sendNobleIBC', error);
return JSON.stringify({
error,
});
}
}
async transaction(
type: TransactionTypes,
paramsInJson: Abacus.Nullable<string>,
@@ -354,6 +409,11 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
callback(result);
break;
}
case TransactionType.SendNobleIBC: {
const result = await this.sendNobleIBC(params);
callback(result);
break;
}
default: {
break;
}
@@ -441,6 +501,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;
}
+19 -5
View File
@@ -26,12 +26,12 @@ import {
} from '@/constants/abacus';
import { DEFAULT_MARKETID } from '@/constants/markets';
import { CURRENT_ABACUS_DEPLOYMENT, type DydxNetwork } from '@/constants/networks';
import { CURRENT_ABACUS_DEPLOYMENT, type DydxNetwork, isMainnet } from '@/constants/networks';
import { CLEARED_SIZE_INPUTS, CLEARED_TRADE_INPUTS } from '@/constants/trade';
import type { RootStore } from '@/state/_store';
import { getInputTradeOptions } from '@/state/inputsSelectors';
import { getTransferInputs } from '@/state/inputsSelectors';
import { setTradeFormInputs } from '@/state/inputs';
import { getInputTradeOptions, getTransferInputs } from '@/state/inputsSelectors';
import AbacusRest from './rest';
import AbacusAnalytics from './analytics';
@@ -43,6 +43,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 +81,13 @@ class AbacusStateManager {
this.abacusFormatter
);
const appConfigs = AbacusAppConfig.Companion.forWeb;
if (!isMainnet) appConfigs.squidVersion = AbacusAppConfig.SquidVersion.V2DepositOnly;
this.stateManager = new AsyncAbacusStateManager(
'',
CURRENT_ABACUS_DEPLOYMENT,
AbacusAppConfig.Companion.forWeb,
appConfigs,
ioImplementations,
uiImplementations,
// @ts-ignore
@@ -132,6 +136,8 @@ class AbacusStateManager {
this.setTradeValue({ value: null, field: TradeInputField.limitPrice });
}
this.store?.dispatch(setTradeFormInputs(CLEARED_TRADE_INPUTS));
if (shouldResetSize) {
this.setTradeValue({ value: null, field: TradeInputField.size });
this.setTradeValue({ value: null, field: TradeInputField.usdcSize });
@@ -139,6 +145,8 @@ class AbacusStateManager {
if (needsLeverage) {
this.setTradeValue({ value: null, field: TradeInputField.leverage });
}
this.store?.dispatch(setTradeFormInputs(CLEARED_SIZE_INPUTS));
}
};
@@ -175,6 +183,12 @@ class AbacusStateManager {
}
};
setNobleWallet = (nobleWallet?: LocalWallet) => {
if (nobleWallet) {
this.chainTransactions.setNobleWallet(nobleWallet);
}
}
setTransfersSourceAddress = (evmAddress: string) => {
this.stateManager.sourceAddress = evmAddress;
};
+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';
}
+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);
+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);
}
+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 '@/hooks/useTestFlags';
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,
+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,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>
);
@@ -58,11 +58,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 +66,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 +91,8 @@ export const WithdrawForm = () => {
[freeCollateral?.current]
);
useEffect(() => setSlippage(isCctp ? 0 : 0.01), [isCctp]);
useEffect(() => {
abacusStateManager.setTransferValue({
field: TransferInputField.type,
@@ -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),
},
})}
/>
),
+19 -20
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,13 +202,10 @@ 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 ?? '',
value: triggerPriceInput ?? '',
decimals: tickSizeDecimals || USD_DECIMALS,
});
}
@@ -220,10 +222,10 @@ 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 ?? '',
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 ?? '',
});
}
@@ -30,8 +30,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 = ({
@@ -66,7 +66,7 @@ export const MarketLeverageInput = ({
const newLeverage = MustBigNumber(floatValue).toFixed();
if (value === '' || newLeverage === 'NaN' || !floatValue) {
setLeverageInputValue(null);
setLeverageInputValue('');
abacusStateManager.setTradeValue({
value: null,
field: TradeInputField.leverage,
@@ -84,7 +84,7 @@ export const MarketLeverageInput = ({
? newLeverageBN.abs().negated()
: newLeverageBN.abs();
setLeverageInputValue(newLeverageSignedBN.toNumber());
setLeverageInputValue(newLeverageSignedBN.toString());
abacusStateManager.setTradeValue({
value: newLeverageSignedBN.toFixed(LEVERAGE_DECIMALS),
@@ -98,11 +98,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,
});
};
@@ -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: (
+24 -18
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();
@@ -50,24 +51,27 @@ export const TradeSizeInputs = () => {
const { needsLeverage } = currentTradeInputOptions || {};
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);
dispatch(setTradeFormInputs({ amountInput: value }));
const newAmount = MustBigNumber(floatValue).toFixed(decimals);
abacusStateManager.setTradeValue({
@@ -77,7 +81,7 @@ export const TradeSizeInputs = () => {
};
const onUSDCInput = ({ value, floatValue }: { value: string; floatValue?: number }) => {
setUsdcInputValue(floatValue);
dispatch(setTradeFormInputs({ usdAmountInput: value }));
const newUsdcAmount = MustBigNumber(floatValue).toFixed();
abacusStateManager.setTradeValue({
@@ -113,7 +117,7 @@ export const TradeSizeInputs = () => {
}
slotRight={isTablet && inputToggleButton}
type={InputType.Number}
value={sizeInputValue || ''}
value={amountInput || ''}
/>
);
@@ -122,7 +126,7 @@ export const TradeSizeInputs = () => {
id="trade-usdc"
onInput={onUSDCInput}
type={InputType.Currency}
value={usdcInputValue || ''}
value={usdAmountInput || ''}
decimals={tickSizeDecimals || USD_DECIMALS}
label={
<>
@@ -153,8 +157,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);
`;
@@ -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;
@@ -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) => {
+15 -9
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()}
/>
</>
)}
@@ -108,6 +109,7 @@ export const MarketsTable = ({ className }: { className?: string }) => {
<Styled.Output
type={OutputType.Percent}
value={MustBigNumber(priceChange24HPercent).abs()}
isPositive={MustBigNumber(priceChange24HPercent).gt(0)}
isNegative={MustBigNumber(priceChange24HPercent).isNegative()}
/>
)}
@@ -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)`};
`;
+22 -20
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';
@@ -208,7 +208,6 @@ const OrderbookTable = ({
// Style props
histogramSide={histogramSide}
hideHeader={hideHeader}
withFocusStickyRows
style={{
'--histogram-range': histogramRange,
}}
@@ -223,11 +222,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) ?? {};
@@ -274,18 +274,16 @@ export const Orderbook = ({
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,
@@ -510,17 +508,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} & {