diff --git a/package.json b/package.json index aaf4322..b7b1aa9 100644 --- a/package.json +++ b/package.json @@ -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.28", - "@dydxprotocol/v4-client-js": "^1.0.0", - "@dydxprotocol/v4-localization": "^1.0.6", + "@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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index df88ec8..bdb47df 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,14 +27,14 @@ dependencies: specifier: ^0.31.0 version: 0.31.0 '@dydxprotocol/v4-abacus': - specifier: ^1.0.28 - version: 1.0.28 + specifier: ^1.1.4 + version: 1.1.4 '@dydxprotocol/v4-client-js': - specifier: ^1.0.0 - version: 1.0.0 - '@dydxprotocol/v4-localization': specifier: ^1.0.6 version: 1.0.6 + '@dydxprotocol/v4-localization': + 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.28: - resolution: {integrity: sha512-JIpGK6++ug9Mb24N+z6AjoZAntIva6Rgl3NxsJw1Ykk/RuvXTYigcDTeknemU6dUhlQ1EJrrsgmvwpqBwNbz5A==} + /@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.6: - resolution: {integrity: sha512-nv/QBKKwixBmhjYmRbA4kSMFznjzYk0MDW8Lq/ht2AUGDnUiBDztxFgYDlxzbf14+1w967NIP0N77GaAZKiFyg==} + /@dydxprotocol/v4-localization@1.0.17: + resolution: {integrity: sha512-fybNpi1ono1IdwFQJTszsHbHmGti2BdhTo1nz9KOmKh3tmXdhWObKb64Ve6/6Fjk6h/9+DeZ5ZG4UbYyYMyhCA==} dev: false /@dydxprotocol/v4-proto@0.4.1: diff --git a/public/configs/cctp.json b/public/configs/cctp.json new file mode 100644 index 0000000..61bd08a --- /dev/null +++ b/public/configs/cctp.json @@ -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" + } +] \ No newline at end of file diff --git a/public/configs/env.json b/public/configs/env.json index 9922dc3..bbd5982 100644 --- a/public/configs/env.json +++ b/public/configs/env.json @@ -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]", diff --git a/public/dots-background.svg b/public/dots-background.svg new file mode 100644 index 0000000..2c1c8e2 --- /dev/null +++ b/public/dots-background.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/logos/chaos-labs.svg b/public/logos/chaos-labs.svg new file mode 100644 index 0000000..383e3f9 --- /dev/null +++ b/public/logos/chaos-labs.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/rewards-stars.svg b/public/rewards-stars.svg new file mode 100644 index 0000000..1416b77 --- /dev/null +++ b/public/rewards-stars.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index d9cf2a1..57d1970 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -22,8 +22,6 @@ import { NotificationsProvider } from '@/hooks/useNotifications'; import { LocalNotificationsProvider } from '@/hooks/useLocalNotifications'; import { RestrictionProvider } from '@/hooks/useRestrictions'; import { SubaccountProvider } from '@/hooks/useSubaccount'; -import { SquidProvider } from '@/hooks/useSquid'; -import { TestFlagsProvider } from '@/hooks/useTestFlags'; import { GuardedMobileRoute } from '@/components/GuardedMobileRoute'; @@ -124,13 +122,11 @@ const providers = [ wrapProvider(QueryClientProvider, { client: queryClient }), wrapProvider(GrazProvider), wrapProvider(WagmiConfig, { config }), - wrapProvider(TestFlagsProvider), wrapProvider(LocaleProvider), wrapProvider(RestrictionProvider), wrapProvider(DydxProvider), wrapProvider(AccountsProvider), wrapProvider(SubaccountProvider), - wrapProvider(SquidProvider), wrapProvider(LocalNotificationsProvider), wrapProvider(NotificationsProvider), wrapProvider(DialogAreaProvider), diff --git a/src/abi/erc20_usdt.json b/src/abi/erc20_usdt.json new file mode 100644 index 0000000..276cdb9 --- /dev/null +++ b/src/abi/erc20_usdt.json @@ -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" } +] diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 855af88..dff6cdb 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -61,7 +61,7 @@ export const Button = forwardRef { diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index 4d09fe8..d5f5d64 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -38,6 +38,7 @@ import { HelpCircleIcon, HideIcon, HistoryIcon, + LeaderboardIcon, LinkOutIcon, LockIcon, LogoShortIcon, @@ -111,6 +112,7 @@ export enum IconName { HelpCircle = 'HelpCircle', Hide = 'Hide', History = 'History', + Leaderboard = 'Leaderboard', LinkOut = 'LinkOut', Lock = 'Lock', LogoShort = 'LogoShort', @@ -185,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, diff --git a/src/components/Input.tsx b/src/components/Input.tsx index 107e559..b74e4f8 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -129,6 +129,7 @@ export const Input = forwardRef( placeholder={placeholder} value={value} // Other + data-1p-ignore // prevent 1Password fill {...otherProps} /> ) : ( @@ -168,6 +169,7 @@ export const Input = forwardRef( value={formattedValue} autoComplete="off" autoCorrect="off" + data-1p-ignore // prevent 1Password fill {...otherProps} /> )} diff --git a/src/constants/dialogs.ts b/src/constants/dialogs.ts index 1982409..7ce2904 100644 --- a/src/constants/dialogs.ts +++ b/src/constants/dialogs.ts @@ -3,6 +3,7 @@ export enum DialogTypes { Deposit = 'Deposit', DisconnectWallet = 'DisconnectWallet', ExchangeOffline = 'ExchangeOffline', + ExternalLink = 'ExternalLink', FillDetails = 'FillDetails', Help = 'Help', ExternalNavKeplr = 'ExternalNavKeplr', diff --git a/src/constants/localStorage.ts b/src/constants/localStorage.ts index 411d917..863f24e 100644 --- a/src/constants/localStorage.ts +++ b/src/constants/localStorage.ts @@ -22,6 +22,7 @@ export enum LocalStorageKey { SelectedTheme = 'dydx.SelectedTheme', SelectedTradeLayout = 'dydx.SelectedTradeLayout', TradingViewChartConfig = 'dydx.TradingViewChartConfig', + HasSeenLaunchIncentives = 'dydx.HasSeenLaunchIncentives', } export const LOCAL_STORAGE_VERSIONS = { diff --git a/src/constants/markets.ts b/src/constants/markets.ts index edb44f9..203e2ce 100644 --- a/src/constants/markets.ts +++ b/src/constants/markets.ts @@ -26,3 +26,34 @@ export enum FundingDirection { ToShort = 'ToShort', ToLong = 'ToLong', } + +export const MARKETS_TO_DISPLAY = [ + 'BTC-USD', + 'ETH-USD', + 'LINK-USD', + 'SOL-USD', + 'MATIC-USD', + 'ATOM-USD', + 'AVAX-USD', + 'APE-USD', + 'XRP-USD', + 'UNI-USD', + 'ADA-USD', + 'TRX-USD', + 'OP-USD', + 'MKR-USD', + 'DOGE-USD', + 'SHIB-USD', + 'COMP-USD', + 'LDO-USD', + 'NEAR-USD', + 'APT-USD', + 'SUI-USD', + 'DOT-USD', + 'ETC-USD', + 'ARB-USD', + 'CRV-USD', + 'BLUR-USD', + 'FIL-USD', + 'XLM-USD', +]; diff --git a/src/constants/notifications.ts b/src/constants/notifications.ts index 333a0a4..5262c83 100644 --- a/src/constants/notifications.ts +++ b/src/constants/notifications.ts @@ -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; }; diff --git a/src/hooks/useAccounts.tsx b/src/hooks/useAccounts.tsx index 96cd7bb..4bf529b 100644 --- a/src/hooks/useAccounts.tsx +++ b/src/hooks/useAccounts.tsx @@ -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 () => { diff --git a/src/hooks/useLocalNotifications.tsx b/src/hooks/useLocalNotifications.tsx index ac7c6a9..5b2742a 100644 --- a/src/hooks/useLocalNotifications.tsx +++ b/src/hooks/useLocalNotifications.tsx @@ -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; diff --git a/src/hooks/useMarketsData.ts b/src/hooks/useMarketsData.ts index 020db64..e9ec038 100644 --- a/src/hooks/useMarketsData.ts +++ b/src/hooks/useMarketsData.ts @@ -1,7 +1,14 @@ import { useMemo } from 'react'; import { useSelector, shallowEqual } from 'react-redux'; -import { MarketFilters, MARKET_FILTER_LABELS, type MarketData } from '@/constants/markets'; +import { + MarketFilters, + MARKET_FILTER_LABELS, + type MarketData, + MARKETS_TO_DISPLAY, +} from '@/constants/markets'; + +import { testFlags } from '@/lib/testFlags'; import { getAssets } from '@/state/assetsSelectors'; import { getPerpetualMarkets } from '@/state/perpetualsSelectors'; @@ -42,7 +49,9 @@ export const useMarketsData = ( }, [allPerpetualMarkets, allAssets]); const filteredMarkets = useMemo(() => { - const filtered = markets.filter(filterFunctions[filter]); + const filtered = markets + .filter(filterFunctions[filter]) + .filter(({ id }) => (testFlags.displayAllMarkets ? true : MARKETS_TO_DISPLAY.includes(id))); if (searchFilter) { return filtered.filter( diff --git a/src/hooks/useNotificationTypes.tsx b/src/hooks/useNotificationTypes.tsx index c5a340b..619a072 100644 --- a/src/hooks/useNotificationTypes.tsx +++ b/src/hooks/useNotificationTypes.tsx @@ -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: , + 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 })} + + ), + TRADING_BLOGPOST: ( + <$Link + href="https://dydx.exchange/blog/v4-full-trading" + target="_blank" + rel="noopener noreferrer" + > + {stringGetter({ key: STRING_KEYS.HERE })} + + ), + }, + }), + 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); +`; diff --git a/src/hooks/useNotifications.tsx b/src/hooks/useNotifications.tsx index 3c12b8b..863d0df 100644 --- a/src/hooks/useNotifications.tsx +++ b/src/hooks/useNotifications.tsx @@ -81,6 +81,7 @@ const useNotificationsContext = () => { setNotificationPreferences({ [NotificationType.AbacusGenerated]: true, [NotificationType.SquidTransfer]: true, + [NotificationType.ReleaseUpdates]: true, version: LOCAL_STORAGE_VERSIONS[LocalStorageKey.NotificationPreferences], }); } diff --git a/src/hooks/useSquid.tsx b/src/hooks/useSquid.tsx deleted file mode 100644 index 1e3b240..0000000 --- a/src/hooks/useSquid.tsx +++ /dev/null @@ -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; -const SquidContext = createContext(undefined); -SquidContext.displayName = '0xSquid'; - -export const SquidProvider = ({ ...props }) => ( - -); - -export const useSquid = () => useContext(SquidContext); diff --git a/src/hooks/useTestFlags.tsx b/src/hooks/useTestFlags.tsx deleted file mode 100644 index 3aedaee..0000000 --- a/src/hooks/useTestFlags.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { createContext, useContext, useEffect, useState } from 'react'; -import { useLocation } from 'react-router-dom'; -import queryString from 'query-string'; - -class TestFlags { - public queryParams: queryString.ParsedQuery; - - constructor() { - this.queryParams = {}; - } - - setQueryParams(flags: queryString.ParsedQuery) { - this.queryParams = flags; - } - - get displayInitializingMarkets() { - return !!this.queryParams.displayInitializingMarkets; - } -} - -export const testFlags = new TestFlags(); - -const useTestFlagsContext = () => { - const [queryParams, setQueryParams] = useState>({}); - const location = useLocation(); - - useEffect(() => { - const parsedQueryParams = queryString.parse(location.search.substring(-1)); - testFlags.setQueryParams(parsedQueryParams); - setQueryParams(parsedQueryParams); - }, []); - - return { - ...queryParams, - }; -}; - -type TestFlagsContextType = ReturnType; -const TestFlagsContext = createContext({} as TestFlagsContextType); -TestFlagsContext.displayName = 'TestFlags'; - -export const TestFlagsProvider = ({ ...props }) => ( - -); - -export const useTestFlags = () => useContext(TestFlagsContext); diff --git a/src/icons/index.ts b/src/icons/index.ts index e6cf1c5..a32913e 100644 --- a/src/icons/index.ts +++ b/src/icons/index.ts @@ -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'; diff --git a/src/icons/leaderboard.svg b/src/icons/leaderboard.svg new file mode 100644 index 0000000..937ee50 --- /dev/null +++ b/src/icons/leaderboard.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/layout/DialogManager.tsx b/src/layout/DialogManager.tsx index e85e6b1..49faff7 100644 --- a/src/layout/DialogManager.tsx +++ b/src/layout/DialogManager.tsx @@ -11,6 +11,7 @@ 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'; @@ -53,6 +54,7 @@ export const DialogManager = () => { [DialogTypes.FillDetails]: , [DialogTypes.Help]: , [DialogTypes.ExternalNavKeplr]: , + [DialogTypes.ExternalLink]: , [DialogTypes.MnemonicExport]: , [DialogTypes.MobileSignIn]: , [DialogTypes.Onboarding]: , diff --git a/src/layout/Footer/FooterDesktop.tsx b/src/layout/Footer/FooterDesktop.tsx index 7a5f6fe..d6f3d1c 100644 --- a/src/layout/Footer/FooterDesktop.tsx +++ b/src/layout/Footer/FooterDesktop.tsx @@ -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 = () => { } > } slotRight={statusPage && } size={ButtonSize.XSmall} state={{ isDisabled: !statusPage }} + href={statusPage} > {label} diff --git a/src/layout/Header/HeaderDesktop.tsx b/src/layout/Header/HeaderDesktop.tsx index bc493ec..97e5fd7 100644 --- a/src/layout/Header/HeaderDesktop.tsx +++ b/src/layout/Header/HeaderDesktop.tsx @@ -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 && , }, { 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); +`; diff --git a/src/lib/abacus/dydxChainTransactions.ts b/src/lib/abacus/dydxChainTransactions.ts index 9940389..425c16d 100644 --- a/src/lib/abacus/dydxChainTransactions.ts +++ b/src/lib/abacus/dydxChainTransactions.ts @@ -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, callback: (p0: Nullable) => 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 { + 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, @@ -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; } diff --git a/src/lib/abacus/index.ts b/src/lib/abacus/index.ts index c643662..b8ffaae 100644 --- a/src/lib/abacus/index.ts +++ b/src/lib/abacus/index.ts @@ -26,13 +26,15 @@ import { } from '@/constants/abacus'; import { DEFAULT_MARKETID } from '@/constants/markets'; -import { CURRENT_ABACUS_DEPLOYMENT, type DydxNetwork } from '@/constants/networks'; +import { CURRENT_ABACUS_DEPLOYMENT, type DydxNetwork, isMainnet } from '@/constants/networks'; import { CLEARED_SIZE_INPUTS, CLEARED_TRADE_INPUTS } from '@/constants/trade'; import type { RootStore } from '@/state/_store'; import { setTradeFormInputs } from '@/state/inputs'; import { getInputTradeOptions, getTransferInputs } from '@/state/inputsSelectors'; +import { testFlags } from '@/lib/testFlags'; + import AbacusRest from './rest'; import AbacusAnalytics from './analytics'; import AbacusWebsocket from './websocket'; @@ -81,10 +83,14 @@ class AbacusStateManager { this.abacusFormatter ); + const appConfigs = AbacusAppConfig.Companion.forWeb; + if (!isMainnet || testFlags.withCCTP) + appConfigs.squidVersion = AbacusAppConfig.SquidVersion.V2DepositOnly; + this.stateManager = new AsyncAbacusStateManager( '', CURRENT_ABACUS_DEPLOYMENT, - AbacusAppConfig.Companion.forWeb, + appConfigs, ioImplementations, uiImplementations, // @ts-ignore @@ -180,6 +186,12 @@ class AbacusStateManager { } }; + setNobleWallet = (nobleWallet?: LocalWallet) => { + if (nobleWallet) { + this.chainTransactions.setNobleWallet(nobleWallet); + } + }; + setTransfersSourceAddress = (evmAddress: string) => { this.stateManager.sourceAddress = evmAddress; }; @@ -223,18 +235,6 @@ class AbacusStateManager { this.abacusFormatter.setLocaleSeparators({ group, decimal }); }; - setTransferStatus = ({ - hash, - fromChainId, - toChainId, - }: { - hash: string; - fromChainId?: string; - toChainId?: string; - }) => { - this.stateManager.transferStatus(hash, fromChainId, toChainId); - }; - // ------ Transactions ------ // placeOrder = ( diff --git a/src/lib/abacus/websocket.ts b/src/lib/abacus/websocket.ts index 8843c4e..3712f14 100644 --- a/src/lib/abacus/websocket.ts +++ b/src/lib/abacus/websocket.ts @@ -10,7 +10,7 @@ import { } from '@/constants/websocket'; import { lastSuccessfulWebsocketRequestByOrigin } from '@/hooks/useAnalytics'; -import { testFlags } from '@/hooks/useTestFlags'; +import { testFlags } from '@/lib/testFlags'; import { subscriptionsByChannelId } from '@/lib/tradingView/dydxfeed/cache'; import { mapCandle } from '@/lib/tradingView/utils'; diff --git a/src/lib/squid.ts b/src/lib/squid.ts new file mode 100644 index 0000000..ec2c413 --- /dev/null +++ b/src/lib/squid.ts @@ -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 => { + 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'; +} diff --git a/src/lib/testFlags.ts b/src/lib/testFlags.ts new file mode 100644 index 0000000..bbb5aed --- /dev/null +++ b/src/lib/testFlags.ts @@ -0,0 +1,31 @@ +class TestFlags { + public queryParams: { [key: string]: string }; + + constructor() { + this.queryParams = {}; + const hash = window.location.hash; + const queryIndex = hash.indexOf('?'); + if (queryIndex === -1) return + + const queryParamsString = hash.substring(queryIndex + 1); + const params = new URLSearchParams(queryParamsString); + + for (const [key, value] of params) { + this.queryParams[key] = value; + } + } + + get displayInitializingMarkets() { + return !!this.queryParams.displayInitializingMarkets; + } + + get displayAllMarkets() { + return !!this.queryParams.displayAllMarkets; + } + + get withCCTP() { + return !!this.queryParams.withCCTP; + } +} + +export const testFlags = new TestFlags(); diff --git a/src/pages/rewards/DYDXBalancePanel.tsx b/src/pages/rewards/DYDXBalancePanel.tsx index ac4803d..f5c6c60 100644 --- a/src/pages/rewards/DYDXBalancePanel.tsx +++ b/src/pages/rewards/DYDXBalancePanel.tsx @@ -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'; @@ -116,12 +117,17 @@ const Styled: Record = {}; 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` diff --git a/src/pages/rewards/LaunchIncentivesPanel.tsx b/src/pages/rewards/LaunchIncentivesPanel.tsx new file mode 100644 index 0000000..5ffc37e --- /dev/null +++ b/src/pages/rewards/LaunchIncentivesPanel.tsx @@ -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 ? ( + } slotRight={}> + + + ) : ( + + + + + + + + ); +}; + +const LaunchIncentivesTitle = () => { + const stringGetter = useStringGetter(); + return ( + + {stringGetter({ + key: STRING_KEYS.LAUNCH_INCENTIVES_TITLE, + params: { + FOR_V4: {stringGetter({ key: STRING_KEYS.FOR_V4 })}, + }, + })} + {stringGetter({ key: STRING_KEYS.NEW })} + + ); +}; + +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 ( + + +
+ {stringGetter({ key: STRING_KEYS.ESTIMATED_REWARDS })} + + {stringGetter({ + key: STRING_KEYS.LAUNCH_INCENTIVES_SEASON_NUM, + params: { + SEASON_NUMBER: 1, + }, + })} + +
+ + + + {data !== undefined && stringGetter({ key: STRING_KEYS.POINTS })} + +
+ + +
+ ); +}; + +const LaunchIncentivesContent = () => { + const stringGetter = useStringGetter(); + const dispatch = useDispatch(); + + return ( + + + {stringGetter({ key: STRING_KEYS.LAUNCH_INCENTIVES_DESCRIPTION })}{' '} + + + + { + dispatch( + openDialog({ + type: DialogTypes.ExternalLink, + dialogProps: { link: 'https://dydx.exchange/blog/v4-full-trading' }, + }) + ); + }} + slotRight={} + > + {stringGetter({ key: STRING_KEYS.ABOUT })} + + { + dispatch( + openDialog({ + type: DialogTypes.ExternalLink, + dialogProps: { link: 'https://community.chaoslabs.xyz/dydx-v4/risk/leaderboard' }, + }) + ); + }} + slotRight={} + slotLeft={} + > + {stringGetter({ key: STRING_KEYS.LEADERBOARD })} + + + + ); +}; + +const Styled: Record = {}; + +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); +`; diff --git a/src/pages/rewards/MigratePanel.tsx b/src/pages/rewards/MigratePanel.tsx index fd822bc..f623082 100644 --- a/src/pages/rewards/MigratePanel.tsx +++ b/src/pages/rewards/MigratePanel.tsx @@ -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 = {}; 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` diff --git a/src/pages/rewards/RewardsPage.tsx b/src/pages/rewards/RewardsPage.tsx index 3bdba80..745d746 100644 --- a/src/pages/rewards/RewardsPage.tsx +++ b/src/pages/rewards/RewardsPage.tsx @@ -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 ( {import.meta.env.VITE_V3_TOKEN_ADDRESS && } - - {isTablet && } + {isTablet ? ( + <> + + + + ) : ( + + + + + )} + + {stringGetter({ key: STRING_KEYS.GOVERNANCE })}} slotRight={panelArrow} @@ -67,8 +79,6 @@ export const RewardsPage = () => { - - {isNotTablet && } ); @@ -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); diff --git a/src/state/configs.ts b/src/state/configs.ts index c90209d..9da0355 100644 --- a/src/state/configs.ts +++ b/src/state/configs.ts @@ -17,6 +17,7 @@ export interface ConfigsState { feeTiers?: kollections.List; 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; diff --git a/src/state/configsSelectors.ts b/src/state/configsSelectors.ts index ec4aef2..d83bd84 100644 --- a/src/state/configsSelectors.ts +++ b/src/state/configsSelectors.ts @@ -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; diff --git a/src/state/inputs.ts b/src/state/inputs.ts index e8f31ef..c168c12 100644 --- a/src/state/inputs.ts +++ b/src/state/inputs.ts @@ -47,7 +47,10 @@ export const inputsSlice = createSlice({ inputErrors: errors?.toArray(), tradeInputs: trade, closePositionInputs: closePosition, - transferInputs: transfer, + transferInputs: { + ...transfer, + isCctp: !!transfer?.isCctp, + } as Nullable, }; }, diff --git a/src/styles/colors.css b/src/styles/colors.css index dc2bab7..d3a6812 100644 --- a/src/styles/colors.css +++ b/src/styles/colors.css @@ -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); diff --git a/src/views/ExchangeBillboards.tsx b/src/views/ExchangeBillboards.tsx index d6ce345..bdb90e4 100644 --- a/src/views/ExchangeBillboards.tsx +++ b/src/views/ExchangeBillboards.tsx @@ -2,7 +2,9 @@ import styled, { type AnyStyledComponent } from 'styled-components'; import { shallowEqual, useSelector } from 'react-redux'; import { STRING_KEYS } from '@/constants/localization'; +import { MARKETS_TO_DISPLAY } from '@/constants/markets'; import { useBreakpoints, useStringGetter } from '@/hooks'; +import { testFlags } from '@/lib/testFlags'; import { breakpoints } from '@/styles'; import { layoutMixins } from '@/styles/layoutMixins'; @@ -32,6 +34,7 @@ export const ExchangeBillboards: React.FC = ({ 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; diff --git a/src/views/dialogs/ExternalLinkDialog.tsx b/src/views/dialogs/ExternalLinkDialog.tsx new file mode 100644 index 0000000..ae2bdb8 --- /dev/null +++ b/src/views/dialogs/ExternalLinkDialog.tsx @@ -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 ( + + +

{stringGetter({ key: STRING_KEYS.LEAVING_WEBSITE_DISCLAIMER })}.

+ +
+
+ ); +}; + +const Styled: Record = {}; + +Styled.Content = styled.div` + ${layoutMixins.flexColumn} + gap: 1rem; + + font: var(--font-base-book); +`; diff --git a/src/views/dialogs/PreferencesDialog.tsx b/src/views/dialogs/PreferencesDialog.tsx index 1d06f7b..8562eb9 100644 --- a/src/views/dialogs/PreferencesDialog.tsx +++ b/src/views/dialogs/PreferencesDialog.tsx @@ -53,6 +53,18 @@ export const usePreferenceMenu = () => { ), onSelect: () => toggleNotifPreference(NotificationType.SquidTransfer), }, + { + value: NotificationType.ReleaseUpdates, + label: "Release Updates", + slotAfter: ( + null} + /> + ), + onSelect: () => toggleNotifPreference(NotificationType.ReleaseUpdates), + } ], }), [stringGetter, enabledNotifs] diff --git a/src/views/forms/AccountManagementForms/DepositForm.tsx b/src/views/forms/AccountManagementForms/DepositForm.tsx index 094f154..4d51f03 100644 --- a/src/views/forms/AccountManagementForms/DepositForm.tsx +++ b/src/views/forms/AccountManagementForms/DepositForm.tsx @@ -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(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(''); diff --git a/src/views/forms/AccountManagementForms/DepositForm/DepositButtonAndReceipt.tsx b/src/views/forms/AccountManagementForms/DepositForm/DepositButtonAndReceipt.tsx index 4072fae..5cd5815 100644 --- a/src/views/forms/AccountManagementForms/DepositForm/DepositButtonAndReceipt.tsx +++ b/src/views/forms/AccountManagementForms/DepositForm/DepositButtonAndReceipt.tsx @@ -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: {stringGetter({ key: STRING_KEYS.TOTAL_FEES })}, - value: typeof summary?.bridgeFee === 'number' && typeof summary?.gasFee === 'number' && ( - - ), + value: , 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), + }, })} /> ), diff --git a/src/views/forms/AccountManagementForms/WithdrawForm.tsx b/src/views/forms/AccountManagementForms/WithdrawForm.tsx index 94f159c..f575366 100644 --- a/src/views/forms/AccountManagementForms/WithdrawForm.tsx +++ b/src/views/forms/AccountManagementForms/WithdrawForm.tsx @@ -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(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(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, diff --git a/src/views/forms/AccountManagementForms/WithdrawForm/WithdrawButtonAndReceipt.tsx b/src/views/forms/AccountManagementForms/WithdrawForm/WithdrawButtonAndReceipt.tsx index ff9c3f2..3a7b467 100644 --- a/src/views/forms/AccountManagementForms/WithdrawForm/WithdrawButtonAndReceipt.tsx +++ b/src/views/forms/AccountManagementForms/WithdrawForm/WithdrawButtonAndReceipt.tsx @@ -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: {stringGetter({ key: STRING_KEYS.TOTAL_FEES })}, - value: typeof summary?.bridgeFee === 'number' && typeof summary?.gasFee === 'number' && ( - - ), + value: , 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), + }, })} /> ), diff --git a/src/views/notifications/TransferStatusNotification/index.tsx b/src/views/notifications/TransferStatusNotification/index.tsx index e94f90b..fdbbcbf 100644 --- a/src/views/notifications/TransferStatusNotification/index.tsx +++ b/src/views/notifications/TransferStatusNotification/index.tsx @@ -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 = ({ }, })} - {error && ( + {hasError && ( {stringGetter({ key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE, @@ -112,7 +113,7 @@ export const TransferStatusNotification = ({ ) : ( {content} - {!isToast && status?.squidTransactionStatus !== 'success' && ( + {!isToast && status?.squidTransactionStatus !== 'success' && !hasError && ( )} @@ -120,8 +121,7 @@ export const TransferStatusNotification = ({ } slotAction={ isToast && - status && - !error && ( + status && ( {