Compare commits

..
Author SHA1 Message Date
Bill He b26106ba20 Fix status empty bug 2023-11-09 14:07:46 -08:00
Bill 944fc6dc95 Handle squid rout errors (#139)
* Handle squid route errors

* bump abacus

* add dep

* address comments

* fix max

* disable slippage editor

* parse error message in abacus
2023-11-09 10:34:22 -08:00
aleka 91a97a1c68 add network select menu in restriction modals (dev) (#137) 2023-11-09 09:10:12 -05:00
aleka c5f736d805 fix deposit dialog logic (#136) 2023-11-08 14:01:16 -05:00
aleka 3769f7078c update readme re env mode (#133) 2023-11-07 20:28:56 -08:00
Jared Vu edc9fadfee Add TestFlags provider and class (#132)
* Add TestFlags provider and class

* import order nit

* markets is an object and not array

* Use null coalescing op
2023-11-07 20:28:42 -08:00
aleka 5e2d426cd1 Send native tokens inclusive fee (#118)
* send dv4tnt inclusive of fees
2023-11-07 22:42:41 -05:00
Bill 766209baf1 Onboarding fixes (#131)
* Onboarding fixes

* address comments

* rebase
2023-11-07 17:18:16 -08:00
aleka 906faa554d Surface close position input errors (#130)
* show abacus close position validation errors

* bump abacus version
2023-11-07 19:11:08 -05:00
John Huang c339eeccde TRCL-3077 Added an environment for testing. Expected behavior - when "v4 Staging Forced Update" is selected in iOS app, it should switch to the forced update screen (#99) 2023-11-07 13:33:06 -08:00
aleka bd03fb73ca update mintscan tx link on transfer history table (#128)
* update mintscan tx link on transfer history table

* bump localization + fix icon button padding in header
2023-11-07 16:00:22 -05:00
Rui 31825a8a52 Change "iOS" to "ios" for consistency (#129) 2023-11-07 11:01:41 -08:00
John Huang 8d5bf1a721 TRCL-3128 TradePlaceOrder and TradeCancelOrder are now tracked by Abacus (#115) 2023-11-07 10:34:06 -08:00
aleka 7f3d57b46a support multiple modes (#125) 2023-11-07 12:13:59 -05:00
Bill c292c84c2c Fix decimal overflow bug (#127)
* Fix decimal overflow bug

* use decimals:
2023-11-06 17:48:19 -08:00
Bill 2d37ff676b do not use TESTNET_CHAIN_ID (#126) 2023-11-06 17:39:02 -08:00
Rui 68f3552569 Add app configuration to env.json (#121) 2023-11-06 13:02:38 -08:00
aleka 18b12591ca Update LICENSE (#123) 2023-11-06 11:10:31 -08:00
Bill 3f963ffb07 Add Env var descriptions to readme (#124)
* Add Env var descriptions to readme

* update

* remove examples

* grammar
2023-11-06 10:50:45 -08:00
aleka aac024fd5e showing negative buying power as - (#119) 2023-11-06 13:31:41 -05:00
Christopher-Li 4913c690ec Update all dev4 endpoints to use https and wss (#117)
* Update all dev endpoints to use https and wss

* dev4
2023-11-06 09:33:22 -08:00
Bill b0db577b28 Configurable squid integrator id (#120)
* Configurable squid integrator id

* bump abacus
2023-11-03 14:18:15 -07:00
47 changed files with 653 additions and 262 deletions
+7 -3
View File
@@ -4,7 +4,11 @@ VITE_ALCHEMY_API_KEY=
VITE_PK_ENCRYPTION_KEY=
VITE_WALLETCONNECT2_PROJECT_ID=
VITE_V3_TOKEN_ADDRESS=
VITE_TOKEN_MIGRATION_URI=
VITE_V3_TOKEN_ADDRESS=0x6d5bb505a4f85c10b122ccc36e30f57e2b86a291
VITE_TOKEN_MIGRATION_URI=https://bridge.dydx.trade/
AMPLITUDE_API_KEY=
BUGSNAG_API_KEY=
IOS_APP_ID=
INTERCOM_APP_ID=
STATUS_PAGE_SCRIPT_URI=
+4 -1
View File
@@ -1,6 +1,9 @@
Copyright (C) 2023 dYdX Trading Inc.
Subject to your compliance with applicable law, you are granted the right to use the Licensed Work (defined below) under the terms of the GNU Affero General Public License as set forth below; provided, however, that if you violate any such applicable law in your use of the Licensed Work, all of your rights and licenses to use (including any rights to reproduce, distribute, install or modify) the Licensed Work will automatically and immediately terminate.
Subject to your compliance with applicable law and the v4 Terms of Use, available at dydx.exchange/legal, you are granted the right to use the Program or Licensed Work (defined below) under the terms of the GNU Affero General Public License as set forth below; provided, however, that if you violate any such applicable law in your use of the Program or Licensed Work, all of your rights and licenses to use (including any rights to reproduce, distribute, install or modify) the Program or Licensed Work will automatically and immediately terminate.
The “Program” or “Licensed Work” shall mean any of the following: dydxprotocol/cosmos-sdk, dydxprotocol/cometbft, dydxprotocol/v4-chain, dydxprotocol/v4-clients, dydxprotocol/v4-web, dydxprotocol/v4-abacus, dydxprotocol/v4-localization, dydxprotocol/v4-documentation, and any dYdX or dYdX Trading Inc. repository reflecting a copy of, or link to, this license.
The GNU Affero General Public License
+20
View File
@@ -69,6 +69,21 @@ You'll need to provide a Wallet Connect project id to enable onboarding and wall
- Create a project on https://cloud.walletconnect.com/app
- Copy over the project ID into this [field](https://github.com/dydxprotocol/v4-web/blob/67ecbd75b43e0c264b7b4d2d9b3d969830b0621c/public/configs/env.json#L822C33-L822C46)
## Part 4: Set Enviornment variables
Set environment variables via `.env`.
- `VITE_BASE_URL` (required): The base URL of the deployment (e.g., `https://www.example.com`).
- `VITE_ALCHEMY_API_KEY` (optional): Add an Alchemy API key for EVM interactions; the app will fall back to public RPCs if not provided.
- `VITE_PK_ENCRYPTION_KEY` (optional): AES encryption key used for signature obfuscation; necessary for enabling the "Remember Me" feature.
- `VITE_V3_TOKEN_ADDRESS` (optional): Address of the V3 $DYDX token.
- `VITE_TOKEN_MIGRATION_URI` (optional): The URL of the token migration website.
- `AMPLITUDE_API_KEY` (optional): Amplitude API key for enabling Amplitude; used with `pnpm run build:inject-amplitude`.
- `BUGSNAG_API_KEY` (optional): API key for enabling Bugsnag; used with `pnpm run build:inject-bugsnag`.
- `IOS_APP_ID` (optional): iOS app ID used for enabling deep linking to the iOS app; used with `pnpm run build:inject-app-deeplinks`.
- `INTERCOM_APP_ID` (optional): Used for enabling Intercom; utilized with `pnpm run build:inject-intercom`.
- `STATUS_PAGE_SCRIPT_URI` (optional): Used for enabling the status page; used with `pnpm run build:inject-statuspage`.
# Deployments
## Deploying with Vercel
@@ -83,6 +98,11 @@ For the "Build & Development Settings", we recommend the following:
- Framework Preset: `Vite`
- Build Command (override): `pnpm run build`
By default, the dev server runs in development mode and the build command runs in production mode. To override the default mode, you can pass in the `--mode` option flag. For example, if you want to build your app for testnet:
```
pnpm run build --mode testnet
```
If you wish to incorporate analytics via Amplitude and Bugsnag, you can use our scripts:
`pnpm run build:inject-amplitude` and `pnpm run build:inject-bugsnag`. You will need to provide your own API keys for these services. In the Environment Variables section, name the variables as `AMPLITUDE_API_KEY` and `BUGSNAG_API_KEY` and provide the respective keys as their values.
+3 -2
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.3",
"@dydxprotocol/v4-abacus": "^1.0.24",
"@dydxprotocol/v4-client-js": "^1.0.0",
"@dydxprotocol/v4-localization": "^1.0.0",
"@dydxprotocol/v4-localization": "^1.0.5",
"@ethersproject/providers": "^5.7.2",
"@js-joda/core": "^5.5.3",
"@radix-ui/react-collapsible": "^1.0.3",
@@ -96,6 +96,7 @@
"long": "^5.2.3",
"luxon": "^3.3.0",
"qr-code-styling": "1.6.0-rc.1",
"query-string": "^8.1.0",
"react": "^18.2.0",
"react-aria": "^3.25.0",
"react-dom": "^18.2.0",
+18 -19
View File
@@ -1,5 +1,9 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies:
'@0xsquid/sdk':
specifier: ^1.10.0
@@ -23,14 +27,14 @@ dependencies:
specifier: ^0.31.0
version: 0.31.0
'@dydxprotocol/v4-abacus':
specifier: ^1.0.3
version: 1.0.3
specifier: ^1.0.24
version: 1.0.24
'@dydxprotocol/v4-client-js':
specifier: ^1.0.0
version: 1.0.0
'@dydxprotocol/v4-localization':
specifier: ^1.0.0
version: 1.0.0
specifier: ^1.0.5
version: 1.0.5
'@ethersproject/providers':
specifier: ^5.7.2
version: 5.7.2
@@ -193,6 +197,9 @@ dependencies:
qr-code-styling:
specifier: 1.6.0-rc.1
version: 1.6.0-rc.1
query-string:
specifier: ^8.1.0
version: 8.1.0
react:
specifier: ^18.2.0
version: 18.2.0
@@ -975,8 +982,8 @@ packages:
resolution: {integrity: sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA==}
dev: true
/@dydxprotocol/v4-abacus@1.0.3:
resolution: {integrity: sha512-GuX37/DMMNSke8ZFcw52II7IVJfUgVGHqoi1Uw2cUb+hgIjjSG3OtbyRWedm5r6nqWq8BNLOexI0VVFB6OMVYg==}
/@dydxprotocol/v4-abacus@1.0.24:
resolution: {integrity: sha512-wDGSjkrc3Se6Ev7UTjPgJV7PiyzZSz2mJwOTZikLwH7W3k1iPYGQmaCd7TudFk8h2aSdlEwNsQBCf0sLoyvHaQ==}
dev: false
/@dydxprotocol/v4-client-js@1.0.0:
@@ -1007,8 +1014,8 @@ packages:
- utf-8-validate
dev: false
/@dydxprotocol/v4-localization@1.0.0:
resolution: {integrity: sha512-U7QjOlzpfSAVEchkNDB3xYRx7okzdeYi83XsnHwI+EHa2quFm/2Y2dhrBkT5fqpZmhcwxb/vG8AZmFSToOf9hA==}
/@dydxprotocol/v4-localization@1.0.5:
resolution: {integrity: sha512-zZm3GFp/ORz9YjigQFVBlcWmaxYY+XEdIw8AWN5nEbsuiA2Jay+/OA7lO9+nV1J1p1T+TgLo9iczIHJsAo3IsA==}
dev: false
/@dydxprotocol/v4-proto@0.4.1:
@@ -7652,7 +7659,7 @@ packages:
resolution: {integrity: sha512-WIdaQ8uW1vIbYvNnAVunkC6yxTrneJC7VQ5UUQ0kuw8b0C0A39KTIpoQHCfc8tV7o9vF4niwRhdXEdfAgQEsQQ==}
dependencies:
cosmos-directory-types: 0.0.6
node-fetch-native: 1.4.0
node-fetch-native: 1.4.1
dev: false
/cosmos-directory-types@0.0.6:
@@ -7871,7 +7878,6 @@ packages:
/decode-uri-component@0.4.1:
resolution: {integrity: sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==}
engines: {node: '>=14.16'}
dev: true
/deep-eql@4.1.3:
resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==}
@@ -8941,7 +8947,6 @@ packages:
/filter-obj@5.1.0:
resolution: {integrity: sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==}
engines: {node: '>=14.16'}
dev: true
/find-cache-dir@3.3.2:
resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==}
@@ -11283,8 +11288,8 @@ packages:
engines: {node: '>=10.5.0'}
dev: true
/node-fetch-native@1.4.0:
resolution: {integrity: sha512-F5kfEj95kX8tkDhUCYdV8dg3/8Olx/94zB8+ZNthFs6Bz31UpUi8Xh40TN3thLwXgrwXry1pEg9lJ++tLWTcqA==}
/node-fetch-native@1.4.1:
resolution: {integrity: sha512-NsXBU0UgBxo2rQLOeWNZqS3fvflWePMECr8CoSWoSTqCqGbVVsvl9vZu1HfQicYN0g5piV9Gh8RTEvo/uP752w==}
dev: false
/node-fetch@2.6.12:
@@ -11872,7 +11877,6 @@ packages:
decode-uri-component: 0.4.1
filter-obj: 5.1.0
split-on-first: 3.0.0
dev: true
/queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@@ -12697,7 +12701,6 @@ packages:
/split-on-first@3.0.0:
resolution: {integrity: sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==}
engines: {node: '>=12'}
dev: true
/split2@4.2.0:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
@@ -14096,7 +14099,3 @@ packages:
/zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
dev: true
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
+94 -3
View File
@@ -1,4 +1,9 @@
{
"apps": {
"ios": {
"scheme": "dydxv4"
}
},
"deployments": {
"MAINNET": {
"environments": [
@@ -38,6 +43,7 @@
"dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
"chain": {
@@ -110,6 +116,7 @@
"dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
"chain": {
@@ -181,6 +188,7 @@
"dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
"chain": {
@@ -200,8 +208,8 @@
"endpoints": {
"indexers": [
{
"api": "http://indexer.v4dev4.dydx.exchange",
"socket": "ws://indexer.v4dev4.dydx.exchange"
"api": "https://indexer.v4dev4.dydx.exchange",
"socket": "wss://indexer.v4dev4.dydx.exchange"
}
],
"validators": [
@@ -252,6 +260,7 @@
"dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
"chain": {
@@ -323,6 +332,7 @@
"dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
"chain": {
@@ -390,12 +400,84 @@
"signTypedDataDomainName": "dYdX Chain"
}
},
"dydxprotocol-staging-forced-update": {
"name": "v4 Staging Forced Update",
"ethereumChainId": "5",
"dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"isMainNet": false,
"tokens": {
"chain": {
"name": "Dv4TNT",
"denom": "adv4tnt",
"decimals": 18,
"image": "/currencies/dydx.png"
},
"usdc": {
"name": "USDC",
"denom": "ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5",
"gasDenom": "uusdc",
"decimals": 6,
"image": "/currencies/usdc.png"
}
},
"endpoints": {
"indexers": [
{
"api": "https://indexer.v4staging.dydx.exchange",
"socket": "wss://indexer.v4staging.dydx.exchange"
}
],
"faucet": "https://faucet.v4staging.dydx.exchange",
"validators": [
"https://validator.v4staging.dydx.exchange"
],
"0xsquid": "https://testnet.api.squidrouter.com"
},
"links": {
"tos": "https://dydx.exchange/v4-terms",
"privacy": "https://dydx.exchange/privacy",
"mintscan": "https://testnet.mintscan.io/dydx-testnet/txs/{tx_hash}",
"mintscanBase": "https://testnet.mintscan.io/dydx-testnet",
"documentation": "https://v4-teacher.vercel.app/",
"community": "https://discord.com/invite/dydx",
"feedback": "https://docs.google.com/forms/d/e/1FAIpQLSezLsWCKvAYDEb7L-2O4wOON1T56xxro9A2Azvl6IxXHP_15Q/viewform",
"blogs": "https://www.dydx.foundation/blog"
},
"wallets": {
"walletconnect": {
"client": {
"name": "dYdX v4",
"description": "dYdX v4 App",
"iconUrl": "/logos/dydx-x.png"
},
"v2": {
"projectId": "47559b2ec96c09aed9ff2cb54a31ab0e"
}
},
"walletSegue": {
"callbackUrl": "/walletsegue"
},
"images": "/wallets/",
"signTypedDataAction": "dYdX Chain Onboarding",
"signTypedDataDomainName": "dYdX Chain"
},
"apps": {
"ios": {
"minimalVersion": "1.0",
"build":40000,
"url": "https://apps.apple.com/app/dydx/id1564787350"
}
}
},
"dydxprotocol-staging-west": {
"name": "v4 Staging West",
"ethereumChainId": "5",
"dydxChainId": "dydxprotocol-testnet",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
"chain": {
@@ -469,6 +551,7 @@
"dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
"chain": {
@@ -546,6 +629,7 @@
"dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
"chain": {
@@ -620,6 +704,7 @@
"dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
"chain": {
@@ -694,6 +779,7 @@
"dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
"chain": {
@@ -768,6 +854,7 @@
"dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
"chain": {
@@ -842,6 +929,7 @@
"dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
"chain": {
@@ -876,6 +964,7 @@
"privacy": "https://dydx.exchange/privacy",
"statusPage": "https://status.v4testnet.dydx.exchange/",
"mintscan": "https://testnet.mintscan.io/dydx-testnet/txs/{tx_hash}",
"mintscanBase": "https://testnet.mintscan.io/dydx-testnet",
"documentation": "https://docs.dydx.exchange/",
"community": "https://discord.com/invite/dydx",
"feedback": "https://docs.google.com/forms/d/e/1FAIpQLSezLsWCKvAYDEb7L-2O4wOON1T56xxro9A2Azvl6IxXHP_15Q/viewform",
@@ -906,6 +995,7 @@
"dydxChainId": "dydx-testnet-4",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"squidIntegratorId": "dYdX-api",
"isMainNet": false,
"tokens": {
"chain": {
@@ -980,6 +1070,7 @@
"dydxChainId": "[mainnet chain id]",
"chainName": "dYdX Chain",
"chainLogo": "dydx-chain.png",
"squidIntegratorId": "[mainnet squid integrator id]",
"isMainNet": true,
"tokens": {
"comment": "Change according to mainnet release",
@@ -1014,7 +1105,7 @@
"tos": "[HTTP link to TOS]",
"privacy": "[HTTP link to Privacy Policy]",
"mintscan": "[HTTP link to Mintscan, with {tx_hash} placeholder]",
"mintscanBase": "[HTTP link to TOS mintsacn base url]",
"mintscanBase": "[HTTP link to TOS mintscan base url]",
"feedback": "[HTTP link to feedback form, can be null]",
"blogs": "[HTTP link to blogs, can be null]",
"foundation": "[HTTP link to foundation, can be null]",
+2
View File
@@ -23,6 +23,7 @@ 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';
@@ -123,6 +124,7 @@ const providers = [
wrapProvider(QueryClientProvider, { client: queryClient }),
wrapProvider(GrazProvider),
wrapProvider(WagmiConfig, { config }),
wrapProvider(TestFlagsProvider),
wrapProvider(LocaleProvider),
wrapProvider(RestrictionProvider),
wrapProvider(DydxProvider),
+12 -6
View File
@@ -1,12 +1,18 @@
import environments from '../../public/configs/env.json';
// TODO: Update for Mainnet deployment
export const AVAILABLE_ENVIRONMENTS =
import.meta.env.MODE === 'production'
? environments.deployments.TESTNET
: environments.deployments.DEV;
const CURRENT_MODE = ({
production: 'MAINNET',
testnet: 'TESTNET',
staging: 'DEV',
development: 'DEV',
}[import.meta.env.MODE] ?? 'MAINNET') as 'MAINNET' | 'TESTNET' | 'DEV';
export const CURRENT_ABACUS_DEPLOYMENT = import.meta.env.MODE === 'production' ? 'TESTNET' : 'DEV';
export const isMainnet = CURRENT_MODE === 'MAINNET';
export const isTestnet = CURRENT_MODE === 'TESTNET';
export const isDev = CURRENT_MODE === 'DEV';
export const AVAILABLE_ENVIRONMENTS = environments.deployments[CURRENT_MODE];
export const CURRENT_ABACUS_DEPLOYMENT = CURRENT_MODE;
export const ENVIRONMENT_CONFIG_MAP = environments.environments;
export type DydxNetwork = keyof typeof ENVIRONMENT_CONFIG_MAP;
export const DEFAULT_APP_ENVIRONMENT = AVAILABLE_ENVIRONMENTS.default as DydxNetwork;
+2
View File
@@ -42,6 +42,7 @@ const useAccountsContext = () => {
selectedWalletError,
evmAddress,
signerWagmi,
publicClientWagmi,
dydxAddress: connectedDydxAddress,
signerGraz,
} = useWalletConnection();
@@ -296,6 +297,7 @@ const useAccountsContext = () => {
// Wallet connection (EVM)
evmAddress,
signerWagmi,
publicClientWagmi,
// Wallet connection (Cosmos)
signerGraz,
+2 -1
View File
@@ -1,3 +1,4 @@
import { isDev } from '@/constants/networks';
import { WalletType } from '@/constants/wallets';
import { isTruthy } from '@/lib/isTruthy';
@@ -6,7 +7,7 @@ export const useDisplayedWallets = () => {
return [
WalletType.MetaMask,
import.meta.env.MODE !== 'production' && WalletType.Keplr,
isDev && WalletType.Keplr,
WalletType.WalletConnect2,
+10 -1
View File
@@ -66,7 +66,16 @@ const useLocalNotificationsContext = () => {
status: currentStatus,
} of transferNotifications) {
try {
if (currentStatus && currentStatus?.squidTransactionStatus !== 'ongoing') continue;
// skip if error is returned or if the transaction is not ongoing
if (
// @ts-ignore status.errors is not in the type definition but can be returned
currentStatus?.errors ||
currentStatus?.error ||
(currentStatus?.squidTransactionStatus &&
currentStatus?.squidTransactionStatus !== 'ongoing')
) {
continue;
}
const status = await squid?.getStatus({ transactionId: txHash, toChainId, fromChainId });
if (status) statuses[txHash] = status;
+4 -2
View File
@@ -7,9 +7,11 @@ import { AlertType } from '@/constants/alerts';
import { AbacusOrderStatus, ORDER_SIDES } from '@/constants/abacus';
import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS, StringKey } from '@/constants/localization';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
import { type NotificationTypeConfig, NotificationType } from '@/constants/notifications';
import { ORDER_SIDE_STRINGS } from '@/constants/trade';
import { useSelectedNetwork } from '@/hooks';
import { useLocalNotifications } from '@/hooks/useLocalNotifications';
import { Icon, IconName } from '@/components/Icon';
@@ -23,7 +25,6 @@ import { OrderStatusIcon } from '@/views/OrderStatusIcon';
import { useStringGetter } from './useStringGetter';
import { TransferStatusSteps } from '@/views/TransferStatusSteps';
import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client-js';
export const notificationTypes = [
{
@@ -124,6 +125,7 @@ export const notificationTypes = [
useTrigger: ({ trigger }) => {
const stringGetter = useStringGetter();
const { transferNotifications } = useLocalNotifications();
const { selectedNetwork } = useSelectedNetwork();
const getTitleStringKey = useCallback((type: 'deposit' | 'withdrawal', finished: boolean) => {
if (type === 'deposit' && !finished) return STRING_KEYS.DEPOSIT_IN_PROGRESS;
@@ -136,7 +138,7 @@ export const notificationTypes = [
for (const transfer of transferNotifications) {
const { fromChainId, status, txHash, toAmount } = transfer;
const finished = Boolean(status) && status?.squidTransactionStatus !== 'ongoing';
const type = fromChainId === TESTNET_CHAIN_ID ? 'withdrawal' : 'deposit';
const type = fromChainId === ENVIRONMENT_CONFIG_MAP[selectedNetwork].dydxChainId ? 'withdrawal' : 'deposit';
// @ts-ignore status.errors is not in the type definition but can be returned
const error = status?.errors?.length ? status?.errors[0] : status?.error;
+6 -2
View File
@@ -8,7 +8,7 @@ import { getSelectedNetwork } from '@/state/appSelectors';
export const NATIVE_TOKEN_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';
export const STATUS_ERROR_GRACE_PERIOD = 120_000;
export const STATUS_ERROR_GRACE_PERIOD = 300_000;
const useSquidContext = () => {
const selectedNetwork = useSelector(getSelectedNetwork);
@@ -22,7 +22,11 @@ const useSquidContext = () => {
};
const squid = useMemo(
() => new Squid({ baseUrl: ENVIRONMENT_CONFIG_MAP[selectedNetwork]?.endpoints['0xsquid'] }),
() =>
new Squid({
baseUrl: ENVIRONMENT_CONFIG_MAP[selectedNetwork]?.endpoints['0xsquid'],
integratorId: ENVIRONMENT_CONFIG_MAP[selectedNetwork]?.squidIntegratorId,
}),
[selectedNetwork]
);
+91 -60
View File
@@ -48,7 +48,7 @@ export const useSubaccount = () => useContext(SubaccountContext);
export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: LocalWallet }) => {
const dispatch = useDispatch();
const { usdcDenom } = useTokenConfigs();
const { usdcDenom, usdcDecimals } = useTokenConfigs();
const { compositeClient, faucetClient } = useDydxClient();
const { getFaucetFunds } = useMemo(
@@ -79,16 +79,34 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
subaccountClient: SubaccountClient;
assetId?: number;
amount: number;
}) => await compositeClient?.depositToSubaccount(subaccountClient, amount.toString()),
}) => {
try {
return await compositeClient?.depositToSubaccount(
subaccountClient,
amount.toFixed(usdcDecimals)
);
} catch (error) {
log('useSubaccount/depositToSubaccount', error);
throw error;
}
},
withdrawFromSubaccount: async ({
subaccountClient,
amount,
}: {
subaccountClient: SubaccountClient;
amount: number;
}) => await compositeClient?.withdrawFromSubaccount(subaccountClient, amount.toString()),
}) => {
try {
return await compositeClient?.withdrawFromSubaccount(
subaccountClient,
amount.toFixed(usdcDecimals)
);
} catch (error) {
log('useSubaccount/withdrawFromSubaccount', error);
throw error;
}
},
transferFromSubaccountToAddress: async ({
subaccountClient,
assetId = 0,
@@ -99,27 +117,33 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
assetId?: number;
amount: number;
recipient: string;
}) =>
await compositeClient?.validatorClient.post.send(
subaccountClient?.wallet,
() =>
new Promise((resolve) => {
const msg =
compositeClient?.validatorClient.post.composer.composeMsgWithdrawFromSubaccount(
subaccountClient.address,
subaccountClient.subaccountNumber,
assetId,
Long.fromNumber(amount * QUANTUM_MULTIPLIER),
recipient
);
}) => {
try {
return await compositeClient?.validatorClient.post.send(
subaccountClient?.wallet,
() =>
new Promise((resolve) => {
const msg =
compositeClient?.validatorClient.post.composer.composeMsgWithdrawFromSubaccount(
subaccountClient.address,
subaccountClient.subaccountNumber,
assetId,
Long.fromNumber(amount * QUANTUM_MULTIPLIER),
recipient
);
resolve([msg]);
}),
false,
undefined,
undefined,
Method.BroadcastTxCommit
),
resolve([msg]);
}),
false,
undefined,
undefined,
Method.BroadcastTxCommit
);
} catch (error) {
log('useSubaccount/transferFromSubaccountToAddress', error);
throw error;
}
},
transferNativeToken: async ({
subaccountClient,
@@ -129,24 +153,30 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
subaccountClient: SubaccountClient;
amount: number;
recipient: string;
}) =>
await compositeClient?.validatorClient.post.send(
subaccountClient.wallet,
() =>
new Promise((resolve) => {
const msg = compositeClient?.sendTokenMessage(
subaccountClient.wallet,
amount.toString(),
recipient,
);
}) => {
try {
return await compositeClient?.validatorClient.post.send(
subaccountClient.wallet,
() =>
new Promise((resolve) => {
const msg = compositeClient?.sendTokenMessage(
subaccountClient.wallet,
amount.toString(),
recipient
);
resolve([msg]);
}),
false,
compositeClient?.validatorClient?.post.defaultDydxGasPrice,
undefined,
Method.BroadcastTxCommit
),
resolve([msg]);
}),
false,
compositeClient?.validatorClient?.post.defaultDydxGasPrice,
undefined,
Method.BroadcastTxCommit
);
} catch (error) {
log('useSubaccount/transferNativeToken', error);
throw error;
}
},
sendSquidWithdrawFromSubaccount: async ({
subaccountClient,
@@ -158,20 +188,27 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
payload: string;
}) => {
if (!compositeClient) throw new Error('client not initialized');
try {
const transaction = JSON.parse(payload);
const transaction = JSON.parse(payload);
const msg = compositeClient.withdrawFromSubaccountMessage(
subaccountClient,
amount.toFixed(usdcDecimals)
);
const ibcMsg: EncodeObject = {
typeUrl: transaction.msgTypeUrl,
value: transaction.msg,
};
const msg = compositeClient.withdrawFromSubaccountMessage(subaccountClient, amount.toString());
const ibcMsg: EncodeObject = {
typeUrl: transaction.msgTypeUrl,
value: transaction.msg,
};
return await compositeClient.send(
subaccountClient.wallet,
() => Promise.resolve([msg, ibcMsg]),
false
);
return await compositeClient.send(
subaccountClient.wallet,
() => Promise.resolve([msg, ibcMsg]),
false
);
} catch (error) {
log('useSubaccount/sendSquidWithdrawFromSubaccount', error);
throw error;
}
},
}),
[compositeClient]
@@ -313,11 +350,6 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
placeOrderParams = abacusStateManager.placeOrder(callback);
}
track(AnalyticsEvent.TradePlaceOrder, {
...placeOrderParams,
isClosePosition,
} as HumanReadablePlaceOrderPayload & { isClosePosition: boolean });
return placeOrderParams;
},
[subaccountClient]
@@ -346,7 +378,6 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
}) => {
const callback = (success: boolean, parsingError?: Nullable<ParsingError>) => {
if (success) {
track(AnalyticsEvent.TradeCancelOrder);
onSuccess?.();
} else {
onError?.({ errorStringKey: parsingError?.stringKey });
+46
View File
@@ -0,0 +1,46 @@
import { createContext, useContext, useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import queryString from 'query-string';
class TestFlags {
public queryParams: queryString.ParsedQuery<string>;
constructor() {
this.queryParams = {};
}
setQueryParams(flags: queryString.ParsedQuery<string>) {
this.queryParams = flags;
}
get displayInitializingMarkets() {
return !!this.queryParams.displayInitializingMarkets;
}
}
export const testFlags = new TestFlags();
const useTestFlagsContext = () => {
const [queryParams, setQueryParams] = useState<queryString.ParsedQuery<string>>({});
const location = useLocation();
useEffect(() => {
const parsedQueryParams = queryString.parse(location.search.substring(-1));
testFlags.setQueryParams(parsedQueryParams);
setQueryParams(parsedQueryParams);
}, []);
return {
...queryParams,
};
};
type TestFlagsContextType = ReturnType<typeof useTestFlagsContext>;
const TestFlagsContext = createContext<TestFlagsContextType>({} as TestFlagsContextType);
TestFlagsContext.displayName = 'TestFlags';
export const TestFlagsProvider = ({ ...props }) => (
<TestFlagsContext.Provider value={useTestFlagsContext()} {...props} />
);
export const useTestFlags = () => useContext(TestFlagsContext);
+4
View File
@@ -19,8 +19,10 @@ export const useTokenConfigs = (): {
},
};
usdcDenom: string;
usdcDecimals: number;
usdcLabel: string;
chainTokenDenom: string;
chainTokenDecimals: number;
chainTokenLabel: string;
} => {
const { selectedNetwork } = useSelectedNetwork();
@@ -29,8 +31,10 @@ export const useTokenConfigs = (): {
return {
tokensConfigs,
usdcDenom: tokensConfigs[DydxChainAsset.USDC].denom,
usdcDecimals: tokensConfigs[DydxChainAsset.USDC].decimals,
usdcLabel: tokensConfigs[DydxChainAsset.USDC].name,
chainTokenDenom: tokensConfigs[DydxChainAsset.CHAINTOKEN].denom,
chainTokenDecimals: tokensConfigs[DydxChainAsset.CHAINTOKEN].decimals,
chainTokenLabel: tokensConfigs[DydxChainAsset.CHAINTOKEN].name,
};
};
+11 -9
View File
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useState, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { LocalStorageKey } from '@/constants/localStorage';
@@ -90,19 +90,21 @@ export const useWalletConnection = () => {
const selectedNetwork = useSelector(getSelectedNetwork);
const walletConnectConfig = ENVIRONMENT_CONFIG_MAP[selectedNetwork].wallets.walletconnect;
const { connectAsync: connectWagmi } =
walletType && walletConnectionType
? useConnectWagmi({
connector: resolveWagmiConnector({
const wagmiConnector = useMemo(
() =>
walletType && walletConnectionType
? resolveWagmiConnector({
walletType,
walletConnection: {
type: walletConnectionType,
},
walletConnectConfig,
}),
})
: useConnectWagmi();
})
: undefined,
[walletConnectConfig, walletType, walletConnectionType]
);
const { connectAsync: connectWagmi } = useConnectWagmi({ connector: wagmiConnector })
const { suggestAndConnect: connectGraz } = useConnectGraz();
const connectWallet = useCallback(
+2 -2
View File
@@ -3,7 +3,7 @@ import styled, { type AnyStyledComponent, css } from 'styled-components';
import { AbacusApiStatus } from '@/constants/abacus';
import { ButtonSize } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
import { ENVIRONMENT_CONFIG_MAP, isDev } from '@/constants/networks';
import { useApiState, useSelectedNetwork, useStringGetter } from '@/hooks';
import { ChatIcon, LinkOutIcon } from '@/icons';
@@ -75,7 +75,7 @@ export const FooterDesktop = () => {
)}
</Styled.Row>
{import.meta.env.MODE !== 'production' && (
{isDev && (
<Styled.Details
withSeparators
items={[
+1
View File
@@ -236,4 +236,5 @@ Styled.IconButton = styled(IconButton)<{ size?: string }>`
${headerMixins.button}
--button-border: none;
--button-icon-size: 1rem;
--button-padding: 0 0.5em;
`;
+10 -10
View File
@@ -30,7 +30,7 @@ import {
import { DialogTypes } from '@/constants/dialogs';
import { UNCOMMITTED_ORDER_TIMEOUT_MS } from '@/constants/trade';
import { QUANTUM_MULTIPLIER } from '@/constants/numbers';
import { ENVIRONMENT_CONFIG_MAP, DydxNetwork, isTestnet } from '@/constants/networks';
import { RootStore } from '@/state/_store';
import { addUncommittedOrderClientId, removeUncommittedOrderClientId } from '@/state/account';
@@ -198,8 +198,12 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
const parsedTx = this.parseToPrimitives(tx);
const hash = parsedTx?.hash;
if (import.meta.env.MODE === 'production') {
console.log(`https://testnet.mintscan.io/dydx-testnet/txs/${hash}`);
if (isTestnet) {
console.log(
`${ENVIRONMENT_CONFIG_MAP[
this.compositeClient.network.getString() as DydxNetwork
]?.links?.mintscan?.replace('{tx_hash}', hash.toString())}`
);
} else console.log(`txHash: ${hash}`);
return JSON.stringify(parsedTx);
@@ -229,7 +233,7 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
orderFlags,
clobPairId,
goodTilBlock || undefined,
goodTilBlockTime || undefined,
goodTilBlockTime || undefined
);
const parsedTx = this.parseToPrimitives(tx);
@@ -295,15 +299,11 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
if (!this.localWallet) {
throw new Error('Missing compositeClient or localWallet');
}
const msg = compositeClient?.sendTokenMessage(
this.localWallet,
amount,
recipient,
);
const msg = compositeClient?.sendTokenMessage(this.localWallet, amount, recipient);
resolve([msg]);
}),
this.compositeClient?.validatorClient?.post.defaultDydxGasPrice,
this.compositeClient?.validatorClient?.post.defaultDydxGasPrice
);
const parsedTx = this.parseToPrimitives(tx);
-5
View File
@@ -1,10 +1,5 @@
import type { AbacusFileSystemProtocol, FileLocation, Nullable } from '@/constants/abacus';
export const ENDPOINTS_PATH =
import.meta.env.MODE === 'production'
? 'config/prod/endpoints.json'
: 'config/staging/dev_endpoints.json';
class AbacusFileSystem implements AbacusFileSystemProtocol {
readTextFile(location: FileLocation, path: string): Nullable<string> {
return null;
+1 -1
View File
@@ -41,7 +41,7 @@ import AbacusStateNotifier from './stateNotification';
import AbacusLocalizer from './localizer';
import AbacusFormatter from './formatter';
import AbacusThreading from './threading';
import AbacusFileSystem, { ENDPOINTS_PATH } from './filesystem';
import AbacusFileSystem from './filesystem';
import { LocaleSeparators } from '../numbers';
class AbacusStateManager {
private store: RootStore | undefined;
+2 -7
View File
@@ -56,13 +56,8 @@ class AbacusRest implements AbacusRestProtocol {
.then(async (response) => {
const data = await response.text();
if (response.ok) {
callback(data, response.status);
} else {
// response not OK, call callback with null data and the status, this includes 400/500 status codes
callback(null, response.status);
}
callback(data, response.status);
try {
lastSuccessfulRestRequestByOrigin[new URL(url).origin] = Date.now();
} catch {}
+23 -4
View File
@@ -1,5 +1,6 @@
import type { AbacusWebsocketProtocol } from '@/constants/abacus';
import type { TradingViewBar } from '@/constants/candles';
import { isDev } from '@/constants/networks';
import {
PING_INTERVAL_MS,
@@ -8,11 +9,12 @@ import {
PONG_MESSAGE_TYPE,
} from '@/constants/websocket';
import { lastSuccessfulWebsocketRequestByOrigin } from '@/hooks/useAnalytics';
import { testFlags } from '@/hooks/useTestFlags';
import { subscriptionsByChannelId } from '@/lib/tradingView/dydxfeed/cache';
import { mapCandle } from '@/lib/tradingView/utils';
import { lastSuccessfulWebsocketRequestByOrigin } from '@/hooks/useAnalytics';
import { log } from '../telemetry';
const RECONNECT_INTERVAL_MS = 10_000;
@@ -143,6 +145,23 @@ class AbacusWebsocket implements Omit<AbacusWebsocketProtocol, '__doNotUseOrImpl
break;
}
case 'v4_markets': {
if (testFlags.displayInitializingMarkets) {
shouldProcess = false;
const { contents } = parsedMessage;
Object.keys(contents.markets ?? {}).forEach((market: any) => {
const status = contents.markets[market].status;
if (status === 'INITIALIZING') {
contents.markets[market].status = 'ONLINE';
}
});
this.receivedCallback?.(JSON.stringify(parsedMessage));
}
break;
}
default: {
break;
}
@@ -161,13 +180,13 @@ class AbacusWebsocket implements Omit<AbacusWebsocketProtocol, '__doNotUseOrImpl
this.socket.onclose = (e) => {
this.connectedCallback?.(false);
if (import.meta.env.MODE === 'production') return;
if (!isDev) return;
console.warn('AbacusStateManager > WS > close > ', e);
};
this.socket.onerror = (e) => {
this.connectedCallback?.(false);
if (import.meta.env.MODE === 'production') return;
if (!isDev) return;
console.error('AbacusStateManager > WS > error > ', e);
};
};
+3 -1
View File
@@ -1,5 +1,7 @@
import { isDev } from "@/constants/networks";
export const log = (location: string, error: Error, metadata?: any) => {
if (import.meta.env.MODE !== 'production') {
if (isDev) {
console.warn('telemetry/log:', { location, error, metadata });
}
@@ -131,7 +131,7 @@ export const AccountDetailsAndHistory = () => {
key: 'BuyingPower',
labelKey: STRING_KEYS.BUYING_POWER,
type: OutputType.Fiat,
value: buyingPower?.current,
value: MustBigNumber(buyingPower?.current).lt(0) ? undefined : buyingPower?.current, // show '-' when buying power is negative
},
].filter(isTruthy);
+2 -1
View File
@@ -2,6 +2,7 @@ import type { PayloadAction } from '@reduxjs/toolkit';
import { AbacusApiStatus } from '@/constants/abacus';
import { DialogTypes } from '@/constants/dialogs';
import { isDev } from '@/constants/networks';
import { setApiState, setSelectedNetwork } from '@/state/app';
import { resetPerpetualsState } from '@/state/perpetuals';
@@ -30,7 +31,7 @@ export default (store: any) => (next: any) => async (action: PayloadAction<any>)
store.dispatch(
openDialog({
type: DialogTypes.ExchangeOffline,
dialogProps: { preventClose: import.meta.env.MODE === 'production' },
dialogProps: { preventClose: !isDev },
})
);
}
+9
View File
@@ -48,6 +48,15 @@ export const getTradeInputErrors = (state: RootState) => {
return currentInput === 'trade' ? getInputErrors(state) : [];
};
/**
* @param state
* @returns input errors for Close position
*/
export const getClosePositionInputErrors = (state: RootState) => {
const currentInput = state.inputs.current;
return currentInput === 'closePosition' ? getInputErrors(state) : [];
};
/**
* @param state
* @returns input errors for Transfer
+1 -1
View File
@@ -2,7 +2,7 @@
:root {
--fontFamily-base: "Satoshi", system-ui, -apple-system, Helvetica, Arial, sans-serif;
--fontFamily-monospace: monospace, var(--fontFamily-base);
--fontFamily-monospace: Courier, monospace, var(--fontFamily-base);
--fontWeight-base-0: 450;
--fontWeight-base-1: 500;
@@ -151,7 +151,10 @@ export const AccountInfoConnectedState = () => {
),
label: stringGetter({ key: STRING_KEYS.BUYING_POWER }),
type: OutputType.Fiat,
value: buyingPower,
value:
MustBigNumber(buyingPower?.current).lt(0) && buyingPower?.postOrder === null
? undefined
: buyingPower,
},
].map(
({
+12 -3
View File
@@ -34,7 +34,7 @@ export const TransferStatusToast = ({
}: ElementProps) => {
const stringGetter = useStringGetter();
const [open, setOpen] = useState<boolean>(false);
const [secondsLeft, setSecondsLeft] = useState<number | undefined>();
const [secondsLeft, setSecondsLeft] = useState<number>(0);
// @ts-ignore status.errors is not in the type definition but can be returned
const error = status?.errors?.length ? status?.errors[0] : status?.error;
@@ -49,14 +49,23 @@ export const TransferStatusToast = ({
if (!status) return <LoadingDots size={3} />;
const inProgressStatusString =
type === 'deposit'
? secondsLeft > 0
? STRING_KEYS.DEPOSIT_STATUS
: STRING_KEYS.DEPOSIT_STATUS_SHORTLY
: secondsLeft > 0
? STRING_KEYS.WITHDRAW_STATUS
: STRING_KEYS.WITHDRAW_STATUS_SHORTLY;
const statusString =
type === 'deposit'
? status?.squidTransactionStatus === 'success'
? STRING_KEYS.DEPOSIT_COMPLETE
: STRING_KEYS.DEPOSIT_STATUS
: inProgressStatusString
: status?.squidTransactionStatus === 'success'
? STRING_KEYS.WITHDRAW_COMPLETE
: STRING_KEYS.WITHDRAW_STATUS;
: inProgressStatusString;
return (
<Styled.Root open={open} onOpenChange={setOpen}>
+13 -4
View File
@@ -2,7 +2,7 @@ import { useMemo } from 'react';
import styled, { css, keyframes, type AnyStyledComponent } from 'styled-components';
import { StatusResponse } from '@0xsquid/sdk';
import { useStringGetter } from '@/hooks';
import { useStringGetter, useSelectedNetwork, useURLConfigs } from '@/hooks';
import { Link } from '@/components/Link';
import { Icon, IconName } from '@/components/Icon';
@@ -11,6 +11,7 @@ import { LoadingSpinner } from '@/components/Loading/LoadingSpinner';
import { layoutMixins } from '@/styles/layoutMixins';
import { STRING_KEYS } from '@/constants/localization';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
type ElementProps = {
status?: StatusResponse;
@@ -26,12 +27,17 @@ enum TransferStatusStep {
export const TransferStatusSteps = ({ status, type }: ElementProps) => {
const stringGetter = useStringGetter();
const { selectedNetwork } = useSelectedNetwork();
const { mintscan: mintscanTxUrl } = useURLConfigs();
const dydxChainId = ENVIRONMENT_CONFIG_MAP[selectedNetwork].dydxChainId;
const { currentStep, steps } = useMemo(() => {
const routeStatus = status?.routeStatus;
const fromChain = status?.fromChain?.chainData?.chainId;
const toChain = status?.toChain?.chainData?.chainId;
const currentStatus = routeStatus?.[routeStatus?.length - 1];
const steps = [
{
label: stringGetter({
@@ -54,12 +60,15 @@ export const TransferStatusSteps = ({ status, type }: ElementProps) => {
},
}),
step: TransferStatusStep.ToChain,
link: status?.toChain?.transactionUrl,
link:
type === 'withdrawal'
? status?.toChain?.transactionUrl
: currentStatus?.chainId === dydxChainId && currentStatus?.txHash
? `${mintscanTxUrl?.replace('{tx_hash}', currentStatus.txHash)}`
: undefined,
},
];
const currentStatus = routeStatus?.[routeStatus?.length - 1];
let currentStep = TransferStatusStep.Bridge;
if (!routeStatus?.length) {
+13 -6
View File
@@ -1,6 +1,7 @@
import { useState } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components';
import { isMainnet } from '@/constants/networks';
import { STRING_KEYS } from '@/constants/localization';
import { useBreakpoints, useStringGetter } from '@/hooks';
@@ -16,7 +17,7 @@ type ElementProps = {
};
export const DepositDialog = ({ setIsOpen }: ElementProps) => {
const [showTestDeposit, setShowTestDeposit] = useState(true);
const [showFaucet, setShowFaucet] = useState(!isMainnet);
const stringGetter = useStringGetter();
const { isMobile } = useBreakpoints();
@@ -27,14 +28,20 @@ export const DepositDialog = ({ setIsOpen }: ElementProps) => {
isOpen
setIsOpen={setIsOpen}
title={stringGetter({ key: STRING_KEYS.DEPOSIT })}
description={showTestDeposit && 'Test funds will be sent directly to your dYdX account.'}
description={showFaucet && 'Test funds will be sent directly to your dYdX account.'}
placement={isMobile ? DialogPlacement.FullScreen : DialogPlacement.Default}
>
<Styled.Content>
{showTestDeposit ? <TestnetDepositForm onDeposit={closeDialog} /> : <DepositForm />}
<Styled.TextToggle onClick={() => setShowTestDeposit(!showTestDeposit)}>
{showTestDeposit ? 'Show deposit form (Under Construction)' : 'Show test faucet'}
</Styled.TextToggle>
{isMainnet || !showFaucet ? (
<DepositForm />
) : (
<TestnetDepositForm onDeposit={closeDialog} />
)}
{!isMainnet && (
<Styled.TextToggle onClick={() => setShowFaucet(!showFaucet)}>
{showFaucet ? 'Show deposit form' : 'Show test faucet'}
</Styled.TextToggle>
)}
</Styled.Content>
</Dialog>
);
+2 -1
View File
@@ -5,6 +5,7 @@ import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { AbacusApiStatus } from '@/constants/abacus';
import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS } from '@/constants/localization';
import { isDev } from '@/constants/networks';
import { useApiState, useStringGetter } from '@/hooks';
import { layoutMixins } from '@/styles/layoutMixins';
@@ -45,7 +46,7 @@ export const ExchangeOfflineDialog = ({ preventClose, setIsOpen }: ElementProps)
>
<Styled.Content>
<p>{statusErrorMessage}</p>
{import.meta.env.MODE !== 'production' && <NetworkSelectMenu />}
{isDev && <NetworkSelectMenu />}
</Styled.Content>
</Dialog>
);
@@ -1,11 +1,13 @@
import styled, { AnyStyledComponent } from 'styled-components';
import { STRING_KEYS } from '@/constants/localization';
import { isDev } from '@/constants/networks';
import { useStringGetter } from '@/hooks';
import { layoutMixins } from '@/styles/layoutMixins';
import { Dialog } from '@/components/Dialog';
import { Icon, IconName } from '@/components/Icon';
import { NetworkSelectMenu } from '@/views/menus/NetworkSelectMenu';
type ElementProps = {
preventClose?: boolean;
@@ -25,6 +27,7 @@ export const RestrictedGeoDialog = ({ preventClose, setIsOpen }: ElementProps) =
>
<Styled.Content>
{stringGetter({ key: STRING_KEYS.REGION_NOT_PERMITTED_SUBTITLE })}
{isDev && <NetworkSelectMenu />}
</Styled.Content>
</Dialog>
);
@@ -1,11 +1,13 @@
import styled, { AnyStyledComponent } from 'styled-components';
import { STRING_KEYS } from '@/constants/localization';
import { isDev } from '@/constants/networks';
import { useStringGetter } from '@/hooks';
import { layoutMixins } from '@/styles/layoutMixins';
import { Dialog } from '@/components/Dialog';
import { Icon, IconName } from '@/components/Icon';
import { NetworkSelectMenu } from '@/views/menus/NetworkSelectMenu';
type ElementProps = {
preventClose?: boolean;
@@ -25,6 +27,7 @@ export const RestrictedWalletDialog = ({ preventClose, setIsOpen }: ElementProps
>
<Styled.Content>
{stringGetter({ key: STRING_KEYS.REGION_NOT_PERMITTED_SUBTITLE })}
{isDev && <NetworkSelectMenu />}
</Styled.Content>
</Dialog>
);
@@ -2,22 +2,21 @@ 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 { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client-js';
import { parseUnits } from 'viem'
import { parseUnits } from 'viem';
import erc20 from '@/abi/erc20.json';
import { TransferInputField, TransferInputTokenResource, TransferType } from '@/constants/abacus';
import { AlertType } from '@/constants/alerts';
import { ButtonSize } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
import { NumberSign } from '@/constants/numbers';
import type { EvmAddress } from '@/constants/wallets';
import { useAccounts, useDebounce, useStringGetter } from '@/hooks';
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 { useWalletConnection } from '@/hooks/useWalletConnection';
import { layoutMixins } from '@/styles/layoutMixins';
import { formMixins } from '@/styles/formMixins';
@@ -54,9 +53,9 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
const stringGetter = useStringGetter();
const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(false);
const { selectedNetwork } = useSelectedNetwork();
const { evmAddress, signerWagmi } = useAccounts();
const { publicClientWagmi } = useWalletConnection();
const { evmAddress, signerWagmi, publicClientWagmi } = useAccounts();
const { addTransferNotification } = useLocalNotifications();
@@ -66,6 +65,8 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
chain: chainIdStr,
resources,
summary,
errors: routeErrors,
errorMessage: routeErrorMessage,
} = useSelector(getTransferInputs, shallowEqual) || {};
const chainId = chainIdStr ? parseInt(chainIdStr) : undefined;
@@ -98,7 +99,7 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
useEffect(() => {
const hasInvalidInput =
debouncedAmountBN.isNaN() || debouncedAmountBN.lte(0) || debouncedAmountBN.gte(balanceBN);
debouncedAmountBN.isNaN() || debouncedAmountBN.lte(0) || debouncedAmountBN.gt(balanceBN);
abacusStateManager.setTransferValue({
value: hasInvalidInput ? 0 : debouncedAmount,
@@ -172,7 +173,8 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
const validateTokenApproval = useCallback(async () => {
if (!signerWagmi || !publicClientWagmi) throw new Error('Missing signer');
if (!sourceToken?.address || !sourceToken.decimals) throw new Error('Missing source token address');
if (!sourceToken?.address || !sourceToken.decimals)
throw new Error('Missing source token address');
if (!sourceChain?.rpc) throw new Error('Missing source chain rpc');
if (!requestPayload?.targetAddress) throw new Error('Missing target address');
if (!requestPayload?.value) throw new Error('Missing transaction value');
@@ -182,11 +184,11 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
address: sourceToken.address as EvmAddress,
abi: erc20,
functionName: 'allowance',
args: [evmAddress as EvmAddress, requestPayload.targetAddress as EvmAddress]
args: [evmAddress as EvmAddress, requestPayload.targetAddress as EvmAddress],
});
const sourceAmountBN = parseUnits(debouncedAmount, sourceToken.decimals);
if (sourceAmountBN > (allowance as bigint)) {
const { request } = await publicClientWagmi.simulateContract({
account: evmAddress,
@@ -194,12 +196,12 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
abi: erc20,
functionName: 'approve',
args: [requestPayload.targetAddress as EvmAddress, sourceAmountBN],
})
});
const approveTx = await signerWagmi.writeContract(request);
await publicClientWagmi.waitForTransactionReceipt({
hash: approveTx,
})
});
}
}, [signerWagmi, sourceToken, sourceChain, requestPayload, publicClientWagmi]);
@@ -229,8 +231,7 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
to: requestPayload.targetAddress as EvmAddress,
data: requestPayload.data as EvmAddress,
gasLimit: BigInt(requestPayload.gasLimit),
value:
requestPayload.routeType !== 'SEND' ? BigInt(requestPayload.value) : undefined,
value: requestPayload.routeType !== 'SEND' ? BigInt(requestPayload.value) : undefined,
};
const txHash = await signerWagmi.sendTransaction(tx);
@@ -239,7 +240,7 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
if (txHash) {
addTransferNotification({
txHash: txHash,
toChainId: TESTNET_CHAIN_ID,
toChainId: ENVIRONMENT_CONFIG_MAP[selectedNetwork].dydxChainId,
fromChainId: chainIdStr || undefined,
toAmount: summary?.usdcSize || undefined,
triggeredAt: Date.now(),
@@ -288,6 +289,15 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
return parseWalletError({ error, stringGetter }).message;
}
if (routeErrors) {
return routeErrorMessage
? stringGetter({
key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE,
params: { ERROR_MESSAGE: routeErrorMessage },
})
: stringGetter({ key: STRING_KEYS.SOMETHING_WENT_WRONG });
}
if (fromAmount) {
if (!chainId) {
return stringGetter({ key: STRING_KEYS.MUST_SPECIFY_CHAIN });
@@ -301,7 +311,16 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
}
return undefined;
}, [error, balance, chainId, fromAmount, sourceToken]);
}, [
error,
routeErrors,
routeErrorMessage,
balance,
chainId,
fromAmount,
sourceToken,
stringGetter,
]);
const isDisabled =
Boolean(errorMessage) ||
@@ -1,5 +1,5 @@
import { type Dispatch, type SetStateAction, useState, type ReactNode } from 'react';
import styled, { type AnyStyledComponent, } from 'styled-components';
import styled, { type AnyStyledComponent } from 'styled-components';
import { shallowEqual, useSelector } from 'react-redux';
import type { RouteData } from '@0xsquid/sdk';
@@ -32,6 +32,8 @@ import { WithReceipt } from '@/components/WithReceipt';
import { getSubaccountBuyingPower, getSubaccountEquity } from '@/state/accountSelectors';
import { getTransferInputs } from '@/state/inputsSelectors';
import { MustBigNumber } from '@/lib/numbers';
import { SlippageEditor } from '../SlippageEditor';
type ElementProps = {
@@ -131,7 +133,7 @@ export const DepositButtonAndReceipt = ({
value: (
<DiffOutput
type={OutputType.Fiat}
value={buyingPower}
value={MustBigNumber(buyingPower).lt(0) ? undefined : buyingPower}
newValue={newBuyingPower}
sign={NumberSign.Positive}
withDiff={Boolean(newBuyingPower) && buyingPower !== newBuyingPower}
@@ -162,6 +164,7 @@ export const DepositButtonAndReceipt = ({
label: <span>{stringGetter({ key: STRING_KEYS.SLIPPAGE })}</span>,
value: (
<SlippageEditor
disabled
slippage={slippage}
setIsEditing={setIsEditingSlipapge}
setSlippage={setSlippage}
@@ -24,11 +24,17 @@ type ElementProps = {
slippage: number;
setIsEditing?: Dispatch<SetStateAction<boolean>>;
setSlippage: (slippage: number) => void;
disabled?: boolean;
};
export type SlippageEditorProps = ElementProps;
export const SlippageEditor = ({ slippage, setIsEditing, setSlippage }: SlippageEditorProps) => {
export const SlippageEditor = ({
disabled,
slippage,
setIsEditing,
setSlippage,
}: SlippageEditorProps) => {
const percentSlippage = slippage * 100;
const [slippageInputValue, setSlippageInputValue] = useState(percentSlippage.toString());
const [editorState, setEditorState] = useState(EditorState.Viewing);
@@ -80,6 +86,10 @@ export const SlippageEditor = ({ slippage, setIsEditing, setSlippage }: Slippage
}
};
if (disabled) {
return <Output type={OutputType.Percent} value={slippage} />;
}
return (
<Styled.WithConfirmationPopover
open={editorState !== EditorState.Viewing}
@@ -3,13 +3,13 @@ import type { ChangeEvent, FormEvent } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components';
import type { NumberFormatValues } from 'react-number-format';
import { shallowEqual, useSelector } from 'react-redux';
import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client-js';
import { isAddress } from 'viem';
import { TransferInputField, TransferInputTokenResource, TransferType } from '@/constants/abacus';
import { AlertType } from '@/constants/alerts';
import { ButtonSize } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
import { NumberSign } from '@/constants/numbers';
import {
@@ -17,6 +17,7 @@ import {
useDebounce,
useDydxClient,
useRestrictions,
useSelectedNetwork,
useStringGetter,
useSubaccount,
} from '@/hooks';
@@ -46,12 +47,12 @@ import { MustBigNumber } from '@/lib/numbers';
import { TokenSelectMenu } from './TokenSelectMenu';
import { WithdrawButtonAndReceipt } from './WithdrawForm/WithdrawButtonAndReceipt';
import { join } from 'path';
export const WithdrawForm = () => {
const stringGetter = useStringGetter();
const [error, setError] = useState<string>();
const [isLoading, setIsLoading] = useState(false);
const { selectedNetwork } = useSelectedNetwork();
const { sendSquidWithdraw } = useSubaccount();
const { freeCollateral } = useSelector(getSubaccount, shallowEqual) || {};
@@ -67,6 +68,8 @@ export const WithdrawForm = () => {
chain: chainIdStr,
address: toAddress,
resources,
errors: routeErrors,
errorMessage: routeErrorMessage,
} = useSelector(getTransferInputs, shallowEqual) || {};
const isValidAddress = toAddress && isAddress(toAddress);
@@ -164,7 +167,7 @@ export const WithdrawForm = () => {
const hash = `0x${Buffer.from(txHash.hash).toString('hex')}`;
addTransferNotification({
txHash: hash,
fromChainId: TESTNET_CHAIN_ID,
fromChainId: ENVIRONMENT_CONFIG_MAP[selectedNetwork].dydxChainId,
toChainId: chainIdStr || undefined,
toAmount: debouncedAmountBN.toNumber(),
triggeredAt: Date.now(),
@@ -279,6 +282,15 @@ export const WithdrawForm = () => {
});
}
if (routeErrors) {
return routeErrorMessage
? stringGetter({
key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE,
params: { ERROR_MESSAGE: routeErrorMessage },
})
: stringGetter({ key: STRING_KEYS.SOMETHING_WENT_WRONG });
}
if (!toAddress) return stringGetter({ key: STRING_KEYS.WITHDRAW_MUST_SPECIFY_ADDRESS });
if (sanctionedAddresses.has(toAddress))
@@ -301,12 +313,15 @@ export const WithdrawForm = () => {
return undefined;
}, [
error,
routeErrors,
routeErrorMessage,
freeCollateralBN,
chainIdStr,
debouncedAmountBN,
toToken,
toAddress,
sanctionedAddresses,
stringGetter,
]);
const isDisabled =
@@ -147,6 +147,7 @@ export const WithdrawButtonAndReceipt = ({
label: <span>{stringGetter({ key: STRING_KEYS.SLIPPAGE })}</span>,
value: (
<SlippageEditor
disabled
slippage={slippage}
setIsEditing={setIsEditingSlipapge}
setSlippage={setSlippage}
+31 -7
View File
@@ -4,8 +4,10 @@ import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import {
ClosePositionInputField,
ValidationError,
type HumanReadablePlaceOrderPayload,
type Nullable,
ErrorType,
} from '@/constants/abacus';
import { AlertType } from '@/constants/alerts';
import { ButtonAction, ButtonShape, ButtonSize, ButtonType } from '@/constants/buttons';
@@ -35,13 +37,14 @@ import { PositionPreview } from '@/views/forms/TradeForm/PositionPreview';
import { getCurrentMarketPositionData } from '@/state/accountSelectors';
import { getCurrentMarketAssetData } from '@/state/assetsSelectors';
import { getInputClosePositionData } from '@/state/inputsSelectors';
import { getClosePositionInputErrors, getInputClosePositionData } from '@/state/inputsSelectors';
import { getCurrentMarketConfig, getCurrentMarketId } from '@/state/perpetualsSelectors';
import { closeDialog } from '@/state/dialogs';
import { getCurrentInput } from '@/state/inputsSelectors';
import abacusStateManager from '@/lib/abacus';
import { MustBigNumber } from '@/lib/numbers';
import { getTradeInputAlert } from '@/lib/tradeData';
const MAX_KEY = 'MAX';
@@ -81,15 +84,38 @@ export const ClosePositionForm = ({
const market = useSelector(getCurrentMarketId);
const { id } = useSelector(getCurrentMarketAssetData, shallowEqual) || {};
const { stepSizeDecimals } = useSelector(getCurrentMarketConfig, shallowEqual) || {};
const { stepSizeDecimals, tickSizeDecimals } =
useSelector(getCurrentMarketConfig, shallowEqual) || {};
const { size: sizeData, summary } = useSelector(getInputClosePositionData, shallowEqual) || {};
const { size, percent } = sizeData || {};
const currentInput = useSelector(getCurrentInput);
const closePositionInputErrors = useSelector(getClosePositionInputErrors, shallowEqual);
const currentPositionData = useSelector(getCurrentMarketPositionData, shallowEqual);
const { size: currentPositionSize } = currentPositionData || {};
const { current: currentSize } = currentPositionSize || {};
const currentSizeBN = MustBigNumber(currentSize).abs();
const hasInputErrors = closePositionInputErrors?.some(
(error: ValidationError) => error.type !== ErrorType.warning
);
const inputAlert = getTradeInputAlert({
abacusInputErrors: closePositionInputErrors ?? [],
stringGetter,
stepSizeDecimals,
tickSizeDecimals,
});
let alertContent;
let alertType = AlertType.Error;
if (closePositionError) {
alertContent = closePositionError;
} else if (inputAlert) {
alertContent = inputAlert?.alertString;
alertType = inputAlert?.type;
}
useEffect(() => {
if (currentStep && currentStep !== MobilePlaceOrderSteps.EditOrder) return;
@@ -182,9 +208,7 @@ export const ClosePositionForm = ({
});
};
const alertMessage = closePositionError && (
<AlertMessage type={AlertType.Error}>{closePositionError}</AlertMessage>
);
const alertMessage = alertContent && <AlertMessage type={alertType}>{alertContent}</AlertMessage>;
const inputs = (
<Styled.InputsColumn>
@@ -265,8 +289,8 @@ export const ClosePositionForm = ({
<PlaceOrderButtonAndReceipt
isLoading={isClosingPosition}
hasValidationErrors={!size}
actionStringKey={!size ? STRING_KEYS.ENTER_AMOUNT : undefined}
hasValidationErrors={hasInputErrors}
actionStringKey={inputAlert?.actionStringKey}
summary={summary ?? undefined}
currentStep={currentStep}
isClosePosition
+57 -49
View File
@@ -61,38 +61,53 @@ export const TransferForm = ({
const stringGetter = useStringGetter();
const { freeCollateral } = useSelector(getSubaccount, shallowEqual) || {};
const { dydxAddress } = useAccounts();
const {
address: recipientAddress,
size,
fee,
} = useSelector(getTransferInputs, shallowEqual) || {};
const { transfer } = useSubaccount();
const { nativeTokenBalance, usdcBalance } = useAccountBalance();
const { selectedNetwork } = useSelectedNetwork();
const { tokensConfigs, usdcLabel, chainTokenLabel } = useTokenConfigs();
// User Input
const [asset, setAsset] = useState<DydxChainAsset>(selectedAsset);
const {
address: recipientAddress,
size,
fee,
token,
} = useSelector(getTransferInputs, shallowEqual) || {};
// Form states
const [error, setError] = useState<string>();
const [isLoading, setIsLoading] = useState(false);
const balance = asset === DydxChainAsset.USDC ? freeCollateral?.current : nativeTokenBalance;
const newBalance =
asset === DydxChainAsset.USDC
? freeCollateral?.postOrder
: MustBigNumber(nativeTokenBalance)
.minus(size?.size ?? 0)
.toNumber();
const amount = asset === DydxChainAsset.USDC ? size?.usdcSize : size?.size;
// temp fix: TODO: reset fees when changing token in Abacus
const [currentFee, setCurrentFee] = useState(fee);
useEffect(() => {
setCurrentFee(fee);
}, [fee]);
const showNotEnoughGasWarning = fee && asset === DydxChainAsset.USDC && usdcBalance < fee;
const asset = (token ?? selectedAsset) as DydxChainAsset;
const isUSDCSelected = asset === DydxChainAsset.USDC;
const amount = isUSDCSelected ? size?.usdcSize : size?.size;
const showNotEnoughGasWarning = fee && isUSDCSelected && usdcBalance < fee;
const balance = isUSDCSelected ? freeCollateral?.current : nativeTokenBalance;
// BN
const newBalanceBN = isUSDCSelected
? MustBigNumber(freeCollateral?.postOrder)
: nativeTokenBalance.minus(size?.size ?? 0);
const amountBN = MustBigNumber(amount);
const balanceBN = MustBigNumber(balance);
const newBalanceBN = MustBigNumber(newBalance);
const onChangeAsset = (asset: DydxChainAsset) => {
setError(undefined);
setCurrentFee(undefined);
if (asset) {
abacusStateManager.setTransferValue({
value: asset,
field: TransferInputField.token,
});
}
};
useEffect(() => {
abacusStateManager.setTransferValue({
@@ -100,19 +115,13 @@ export const TransferForm = ({
field: TransferInputField.type,
});
onChangeAsset(selectedAsset);
return () => {
abacusStateManager.clearTransferInputValues();
};
}, []);
useEffect(() => {
setError(undefined);
abacusStateManager.setTransferValue({
value: asset,
field: TransferInputField.token,
});
}, [asset]);
const { sanctionedAddresses } = useRestrictions();
const isAddressValid = useMemo(
@@ -134,11 +143,6 @@ export const TransferForm = ({
setError(undefined);
try {
// Subtract fees from amount if sending native tokens
const amountToTransfer = (
asset === DydxChainAsset.CHAINTOKEN ? amountBN.minus(fee) : amountBN
).toNumber();
const screenResults = await screenAddresses({
addresses: [recipientAddress!, dydxAddress!],
});
@@ -157,9 +161,9 @@ export const TransferForm = ({
);
} else {
const txResponse = await transfer(
amountToTransfer,
amountBN.toNumber(),
recipientAddress as string,
tokensConfigs[asset].denom,
tokensConfigs[asset]?.denom
);
if (txResponse?.code === 0) {
@@ -200,7 +204,7 @@ export const TransferForm = ({
const onChangeAmount = (value?: number) => {
abacusStateManager.setTransferValue({
value,
field: asset === DydxChainAsset.USDC ? TransferInputField.usdcSize : TransferInputField.size,
field: isUSDCSelected ? TransferInputField.usdcSize : TransferInputField.size,
});
};
@@ -226,7 +230,7 @@ export const TransferForm = ({
value: DydxChainAsset.CHAINTOKEN,
label: (
<Styled.InlineRow>
{/* <AssetIcon symbol="DYDX" /> */}
<AssetIcon symbol={chainTokenLabel} />
{chainTokenLabel}
</Styled.InlineRow>
),
@@ -249,15 +253,15 @@ export const TransferForm = ({
key: 'amount',
label: (
<span>
{stringGetter({ key: STRING_KEYS.AVAILABLE })} <Tag>{tokensConfigs[asset].name}</Tag>
{stringGetter({ key: STRING_KEYS.AVAILABLE })} <Tag>{tokensConfigs[asset]?.name}</Tag>
</span>
),
value: (
<DiffOutput
type={OutputType.Asset}
value={balanceBN.toString()}
newValue={newBalanceBN.toString()}
value={balanceBN}
sign={NumberSign.Negative}
newValue={newBalanceBN}
hasInvalidNewValue={newBalanceBN.isNegative()}
withDiff={Boolean(amount && balance) && !amountBN.isNaN()}
/>
@@ -340,8 +344,8 @@ export const TransferForm = ({
<Styled.SelectMenu
label={stringGetter({ key: STRING_KEYS.ASSET })}
value={asset}
onValueChange={setAsset}
value={token}
onValueChange={onChangeAsset}
disabled={isLoading}
>
{assetOptions.map(({ value, label }) => (
@@ -355,12 +359,16 @@ export const TransferForm = ({
type={InputType.Number}
onChange={({ floatValue }: NumberFormatValues) => onChangeAmount(floatValue)}
value={amount ?? undefined}
slotRight={renderFormInputButton({
label: stringGetter({ key: STRING_KEYS.MAX }),
isInputEmpty: size?.usdcSize == null,
onClear: () => onChangeAmount(undefined),
onClick: () => onChangeAmount(balanceBN.toNumber()),
})}
slotRight={
isUSDCSelected &&
balanceBN.gt(0) &&
renderFormInputButton({
label: stringGetter({ key: STRING_KEYS.MAX }),
isInputEmpty: size?.usdcSize == null,
onClear: () => onChangeAmount(undefined),
onClick: () => onChangeAmount(balanceBN.toNumber()),
})
}
disabled={isLoading}
/>
</WithDetailsReceipt>
@@ -369,7 +377,7 @@ export const TransferForm = ({
<AlertMessage type={AlertType.Warning}>
{stringGetter({
key: STRING_KEYS.TRANSFER_INSUFFICIENT_GAS,
params: { USDC_BALANCE: `(${usdcBalance})` },
params: { USDC_BALANCE: `(${usdcBalance} USDC)` },
})}
</AlertMessage>
)}
@@ -379,9 +387,9 @@ export const TransferForm = ({
<Styled.Footer>
<TransferButtonAndReceipt
selectedAsset={asset}
fees={fee || undefined}
isDisabled={!isAmountValid || !isAddressValid || !fee || isLoading}
isLoading={isLoading || Boolean(isAmountValid && isAddressValid && !fee)}
fee={currentFee || undefined}
isDisabled={!isAmountValid || !isAddressValid || !currentFee || isLoading}
isLoading={isLoading || Boolean(isAmountValid && isAddressValid && !currentFee)}
/>
</Styled.Footer>
</Styled.Form>
@@ -23,14 +23,14 @@ import { MustBigNumber } from '@/lib/numbers';
type ElementProps = {
selectedAsset: DydxChainAsset;
fees?: number;
fee?: number;
isDisabled?: boolean;
isLoading?: boolean;
};
export const TransferButtonAndReceipt = ({
selectedAsset,
fees,
fee,
isDisabled,
isLoading,
}: ElementProps) => {
@@ -47,43 +47,46 @@ export const TransferButtonAndReceipt = ({
const { current: equity, postOrder: newEquity } = equityInfo || {};
const { current: leverage, postOrder: newLeverage } = leverageInfo || {};
// TODO(@aforaleka): add wallet balance for DYDX
const balance = selectedAsset === DydxChainAsset.USDC ? equity : nativeTokenBalance;
const newBalance =
selectedAsset === DydxChainAsset.USDC
? newEquity
: MustBigNumber(nativeTokenBalance)
.minus(size?.size ?? 0)
.toNumber();
const isUSDCSelected = selectedAsset === DydxChainAsset.USDC;
const balance = isUSDCSelected ? equity : nativeTokenBalance;
const newNativeTokenBalance = nativeTokenBalance
.minus(size?.size ?? 0)
.minus(fee ?? 0) // show balance after fees for button receipt
.toNumber();
const newBalance = isUSDCSelected ? newEquity : newNativeTokenBalance;
const transferDetailItems = [
{
key: 'fees',
label: (
<span>
{stringGetter({ key: STRING_KEYS.FEES })} <Tag>{tokensConfigs[selectedAsset].name}</Tag>
{stringGetter({ key: STRING_KEYS.FEES })} <Tag>{tokensConfigs[selectedAsset]?.name}</Tag>
</span>
),
value: <Output type={OutputType.Asset} value={fees} />,
value: <Output type={OutputType.Asset} value={fee} />,
},
{
key: 'balance',
label: (
<span>
{stringGetter({ key: STRING_KEYS.BALANCE })} <Tag>{tokensConfigs[selectedAsset].name}</Tag>
{stringGetter({ key: STRING_KEYS.BALANCE })}{' '}
<Tag>{tokensConfigs[selectedAsset]?.name}</Tag>
</span>
),
value: (
<DiffOutput
type={OutputType.Asset}
value={balance}
newValue={newBalance}
sign={NumberSign.Negative}
withDiff={Boolean(newBalance) && balance !== newBalance}
newValue={newBalance}
hasInvalidNewValue={MustBigNumber(newBalance).isNegative()}
withDiff={newBalance !== null && !MustBigNumber(balance).eq(newBalance ?? 0)}
/>
),
},
selectedAsset === DydxChainAsset.USDC && {
isUSDCSelected && {
key: 'leverage',
label: <span>{stringGetter({ key: STRING_KEYS.LEVERAGE })}</span>,
value: (
+23 -8
View File
@@ -36,6 +36,7 @@ import { truncateAddress } from '@/lib/wallet';
import { layoutMixins } from '@/styles/layoutMixins';
import { headerMixins } from '@/styles/headerMixins';
import { MustBigNumber } from '@/lib/numbers';
export const AccountMenu = () => {
const stringGetter = useStringGetter();
@@ -49,6 +50,8 @@ export const AccountMenu = () => {
const { evmAddress, walletType, dydxAddress, hdKey } = useAccounts();
const usdcBalance = freeCollateral?.current || 0;
const onRecoverKeys = () => {
dispatch(openDialog({ type: DialogTypes.Onboarding }));
};
@@ -122,7 +125,11 @@ export const AccountMenu = () => {
</Styled.label>
<Styled.BalanceOutput type={OutputType.Asset} value={nativeTokenBalance} />
</div>
<AssetActions asset={DydxChainAsset.CHAINTOKEN} dispatch={dispatch} />
<AssetActions
asset={DydxChainAsset.CHAINTOKEN}
dispatch={dispatch}
hasBalance={nativeTokenBalance.gt(0)}
/>
</div>
<div>
<div>
@@ -135,11 +142,16 @@ export const AccountMenu = () => {
</Styled.label>
<Styled.BalanceOutput
type={OutputType.Asset}
value={freeCollateral?.current || 0}
value={usdcBalance}
fractionDigits={2}
/>
</div>
<AssetActions asset={DydxChainAsset.USDC} dispatch={dispatch} withOnboarding />
<AssetActions
asset={DydxChainAsset.USDC}
dispatch={dispatch}
hasBalance={MustBigNumber(usdcBalance).gt(0)}
withOnboarding
/>
</div>
</Styled.Balances>
</Styled.AccountInfo>
@@ -217,22 +229,25 @@ const AssetActions = memo(
asset,
dispatch,
withOnboarding,
hasBalance,
}: {
asset: DydxChainAsset;
dispatch: Dispatch;
withOnboarding?: boolean;
hasBalance?: boolean;
}) => (
<Styled.InlineRow>
{[
withOnboarding && {
dialogType: DialogTypes.Withdraw,
iconName: IconName.Withdraw,
},
withOnboarding &&
hasBalance && {
dialogType: DialogTypes.Withdraw,
iconName: IconName.Withdraw,
},
withOnboarding && {
dialogType: DialogTypes.Deposit,
iconName: IconName.Deposit,
},
{
hasBalance && {
dialogType: DialogTypes.Transfer,
dialogProps: { selectedAsset: asset },
iconName: IconName.Send,
+2
View File
@@ -33,6 +33,8 @@ const Styled: Record<string, AnyStyledComponent> = {};
Styled.DropdownSelectMenu = styled(DropdownSelectMenu)`
${headerMixins.dropdownTrigger}
width: max-content;
& > span:first-of-type {
${layoutMixins.textOverflow}
max-width: 5.625rem;
+10 -3
View File
@@ -7,7 +7,7 @@ import { ButtonAction } from '@/constants/buttons';
import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS, StringGetterFunction } from '@/constants/localization';
import { useBreakpoints, useStringGetter } from '@/hooks';
import { useBreakpoints, useStringGetter, useURLConfigs } from '@/hooks';
import { layoutMixins } from '@/styles/layoutMixins';
import { tradeViewMixins } from '@/styles/tradeViewMixins';
@@ -40,11 +40,13 @@ const getTransferHistoryTableColumnDef = ({
key,
stringGetter,
width,
mintscanTxUrl,
}: {
key: TransferHistoryTableColumnKey;
isTablet?: boolean;
stringGetter: StringGetterFunction;
width?: ColumnSize;
mintscanTxUrl?: string;
}): ColumnDef<SubaccountTransfer> => ({
width,
...(
@@ -98,9 +100,12 @@ const getTransferHistoryTableColumnDef = ({
columnKey: TransferHistoryTableColumnKey.TxHash,
getCellValue: (row) => row.transactionHash,
label: stringGetter({ key: STRING_KEYS.TRANSACTION }),
renderCell: ({ transactionHash, resources }) =>
renderCell: ({ transactionHash }) =>
transactionHash ? (
<Styled.TxHash withIcon href={resources.blockExplorerUrl}>
<Styled.TxHash
withIcon
href={`${mintscanTxUrl?.replace('{tx_hash}', transactionHash)}`}
>
{truncateAddress(transactionHash, '')}
</Styled.TxHash>
) : (
@@ -130,6 +135,7 @@ export const TransferHistoryTable = ({
const stringGetter = useStringGetter();
const dispatch = useDispatch();
const { isMobile, isTablet } = useBreakpoints();
const { mintscan: mintscanTxUrl } = useURLConfigs();
const canAccountTrade = useSelector(calculateCanAccountTrade, shallowEqual);
@@ -146,6 +152,7 @@ export const TransferHistoryTable = ({
isTablet,
stringGetter,
width: columnWidths?.[key],
mintscanTxUrl,
})
)}
slotEmpty={