Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0189fac442 | ||
|
|
610c583246 | ||
|
|
d4bcfc0428 | ||
|
|
7dec32df5e | ||
|
|
e09d574d5b | ||
|
|
944fc6dc95 | ||
|
|
91a97a1c68 | ||
|
|
c5f736d805 | ||
|
|
3769f7078c | ||
|
|
edc9fadfee | ||
|
|
5e2d426cd1 | ||
|
|
766209baf1 |
@@ -98,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
@@ -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.19",
|
||||
"@dydxprotocol/v4-abacus": "^1.0.24",
|
||||
"@dydxprotocol/v4-client-js": "^1.0.0",
|
||||
"@dydxprotocol/v4-localization": "^1.0.3",
|
||||
"@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",
|
||||
|
||||
Generated
+15
-16
@@ -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.19
|
||||
version: 1.0.19
|
||||
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.3
|
||||
version: 1.0.3
|
||||
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.19:
|
||||
resolution: {integrity: sha512-XhvSHGr503gNwHWEiOYP7M2uYeLu+Qm4szpE5w5H6hWFCanSGz6zAsTbPuyPLtt7IE1gj5z8mlL9J3tw2AZROg==}
|
||||
/@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.3:
|
||||
resolution: {integrity: sha512-DSTVabwVI6TjR0Z6Nlcl+ryrmayCoki0eBbBcSO/bsNYAffsPAy1e4ctajH49hQC6CKaCyK4urZPEXsdCmLlzQ==}
|
||||
/@dydxprotocol/v4-localization@1.0.5:
|
||||
resolution: {integrity: sha512-zZm3GFp/ORz9YjigQFVBlcWmaxYY+XEdIw8AWN5nEbsuiA2Jay+/OA7lO9+nV1J1p1T+TgLo9iczIHJsAo3IsA==}
|
||||
dev: false
|
||||
|
||||
/@dydxprotocol/v4-proto@0.4.1:
|
||||
@@ -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==}
|
||||
@@ -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
|
||||
|
||||
@@ -439,6 +439,7 @@
|
||||
"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",
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import styled, { type AnyStyledComponent, css } from 'styled-components';
|
||||
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
@@ -22,6 +22,11 @@ export const SearchInput = ({
|
||||
}: SearchInputProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [value, setValue] = useState('');
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) inputRef?.current?.focus();
|
||||
}, [inputRef, isOpen]);
|
||||
|
||||
return (
|
||||
<Styled.Search>
|
||||
@@ -41,6 +46,7 @@ export const SearchInput = ({
|
||||
/>
|
||||
<Styled.Input
|
||||
autoFocus
|
||||
ref={inputRef}
|
||||
value={value}
|
||||
isOpen={isOpen}
|
||||
type={type}
|
||||
|
||||
@@ -831,6 +831,7 @@ Styled.SortArrow = styled.span<{ sortDirection: 'ascending' | 'descending' }>`
|
||||
Styled.Thead = styled.thead<StyleProps>`
|
||||
${layoutMixins.stickyHeader}
|
||||
scroll-snap-align: none;
|
||||
font: var(--font-mini-book);
|
||||
|
||||
> * {
|
||||
height: var(--stickyArea-topHeight);
|
||||
@@ -839,18 +840,17 @@ Styled.Thead = styled.thead<StyleProps>`
|
||||
color: var(--tableHeader-textColor);
|
||||
background-color: var(--tableHeader-backgroundColor);
|
||||
|
||||
@media ${breakpoints.notTablet} {
|
||||
${({ withInnerBorders, withGradientCardRows }) =>
|
||||
withInnerBorders &&
|
||||
!withGradientCardRows &&
|
||||
css`
|
||||
${layoutMixins.withInnerHorizontalBorders}
|
||||
`}
|
||||
}
|
||||
${({ withInnerBorders, withGradientCardRows }) =>
|
||||
withInnerBorders &&
|
||||
!withGradientCardRows &&
|
||||
css`
|
||||
${layoutMixins.withInnerHorizontalBorders}
|
||||
`}
|
||||
`;
|
||||
|
||||
Styled.Tbody = styled.tbody<StyleProps>`
|
||||
${layoutMixins.stickyArea2}
|
||||
font: var(--font-small-book);
|
||||
|
||||
// If <table> height is fixed with not enough rows to overflow, vertically center the rows
|
||||
&:before,
|
||||
@@ -869,6 +869,10 @@ Styled.Tbody = styled.tbody<StyleProps>`
|
||||
--stickyArea2-paddingBottom: var(--border-width);
|
||||
--stickyArea2-paddingLeft: var(--border-width);
|
||||
--stickyArea2-paddingRight: var(--border-width);
|
||||
|
||||
tr:first-of-type {
|
||||
box-shadow: none;
|
||||
}
|
||||
`}
|
||||
|
||||
${({ withGradientCardRows }) =>
|
||||
|
||||
@@ -42,6 +42,7 @@ const useAccountsContext = () => {
|
||||
selectedWalletError,
|
||||
evmAddress,
|
||||
signerWagmi,
|
||||
publicClientWagmi,
|
||||
dydxAddress: connectedDydxAddress,
|
||||
signerGraz,
|
||||
} = useWalletConnection();
|
||||
@@ -135,8 +136,8 @@ const useAccountsContext = () => {
|
||||
getSubaccounts: async ({ dydxAddress }: { dydxAddress: DydxAddress }) => {
|
||||
try {
|
||||
const response = await compositeClient?.indexerClient.account.getSubaccounts(dydxAddress);
|
||||
setDydxSubaccounts(response.subaccounts);
|
||||
return response.subaccounts;
|
||||
setDydxSubaccounts(response?.subaccounts);
|
||||
return response?.subaccounts ?? [];
|
||||
} catch (error) {
|
||||
// 404 is expected if the user has no subaccounts
|
||||
if (error.status === 404) {
|
||||
@@ -296,6 +297,7 @@ const useAccountsContext = () => {
|
||||
// Wallet connection (EVM)
|
||||
evmAddress,
|
||||
signerWagmi,
|
||||
publicClientWagmi,
|
||||
|
||||
// Wallet connection (Cosmos)
|
||||
signerGraz,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
@@ -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(
|
||||
|
||||
@@ -200,10 +200,9 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
|
||||
|
||||
if (isTestnet) {
|
||||
console.log(
|
||||
`${
|
||||
ENVIRONMENT_CONFIG_MAP[this.compositeClient.network.getString() as DydxNetwork]?.links
|
||||
?.mintscanBase
|
||||
}/txs/${hash}`
|
||||
`${ENVIRONMENT_CONFIG_MAP[
|
||||
this.compositeClient.network.getString() as DydxNetwork
|
||||
]?.links?.mintscan?.replace('{tx_hash}', hash.toString())}`
|
||||
);
|
||||
} else console.log(`txHash: ${hash}`);
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -9,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;
|
||||
@@ -144,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;
|
||||
}
|
||||
|
||||
@@ -304,10 +304,6 @@ Styled.FeeTable = styled(Table)`
|
||||
|
||||
@media ${breakpoints.notTablet} {
|
||||
--tableHeader-backgroundColor: var(--color-layer-1);
|
||||
|
||||
thead tr {
|
||||
--border-width: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { useEffect } from 'react';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
import styled, { type AnyStyledComponent } from 'styled-components';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { OnboardingState } from '@/constants/account';
|
||||
import { DialogTypes } from '@/constants/dialogs';
|
||||
import { STRING_KEYS } from '@/constants/localization';
|
||||
import { HistoryRoute, PortfolioRoute } from '@/constants/routes';
|
||||
|
||||
import { useBreakpoints, useDocumentTitle, useStringGetter } from '@/hooks';
|
||||
import { useAccountBalance, useBreakpoints, useDocumentTitle, useStringGetter } from '@/hooks';
|
||||
|
||||
import { FillsTable, FillsTableColumnKey } from '@/views/tables/FillsTable';
|
||||
import { FundingPaymentsTable } from '@/views/tables/FundingPaymentsTable';
|
||||
@@ -14,6 +16,9 @@ import { Icon, IconName } from '@/components/Icon';
|
||||
import { NavigationMenu } from '@/components/NavigationMenu';
|
||||
import { WithSidebar } from '@/components/WithSidebar';
|
||||
|
||||
import { getOnboardingState, getSubaccount } from '@/state/accountSelectors';
|
||||
import { openDialog } from '@/state/dialogs';
|
||||
|
||||
import { PortfolioNavMobile } from './PortfolioNavMobile';
|
||||
import { Overview } from './Overview';
|
||||
import { Positions } from './Positions';
|
||||
@@ -22,11 +27,20 @@ import { Fees } from './Fees';
|
||||
import { History } from './History';
|
||||
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
import { Button } from '@/components/Button';
|
||||
import { ButtonAction } from '@/constants/buttons';
|
||||
|
||||
export default () => {
|
||||
const dispatch = useDispatch();
|
||||
const stringGetter = useStringGetter();
|
||||
const { isTablet, isNotTablet } = useBreakpoints();
|
||||
|
||||
const onboardingState = useSelector(getOnboardingState);
|
||||
const { freeCollateral } = useSelector(getSubaccount, shallowEqual) || {};
|
||||
const { nativeTokenBalance } = useAccountBalance();
|
||||
|
||||
const usdcBalance = freeCollateral?.current || 0;
|
||||
|
||||
useDocumentTitle(stringGetter({ key: STRING_KEYS.PORTFOLIO }));
|
||||
|
||||
const routesComponent = (
|
||||
@@ -84,46 +98,74 @@ export default () => {
|
||||
<WithSidebar
|
||||
sidebar={
|
||||
isTablet ? null : (
|
||||
<Styled.NavigationMenu
|
||||
items={[
|
||||
{
|
||||
group: 'views',
|
||||
groupLabel: stringGetter({ key: STRING_KEYS.VIEWS }),
|
||||
items: [
|
||||
{
|
||||
value: PortfolioRoute.Overview,
|
||||
slotBefore: <Styled.Icon iconName={IconName.Overview} />,
|
||||
label: stringGetter({ key: STRING_KEYS.OVERVIEW }),
|
||||
href: PortfolioRoute.Overview,
|
||||
},
|
||||
{
|
||||
value: PortfolioRoute.Positions,
|
||||
slotBefore: <Styled.Icon iconName={IconName.Cube} />,
|
||||
label: stringGetter({ key: STRING_KEYS.POSITIONS }),
|
||||
href: PortfolioRoute.Positions,
|
||||
},
|
||||
{
|
||||
value: PortfolioRoute.Orders,
|
||||
slotBefore: <Styled.Icon iconName={IconName.OrderPending} />,
|
||||
label: stringGetter({ key: STRING_KEYS.ORDERS }),
|
||||
href: PortfolioRoute.Orders,
|
||||
},
|
||||
{
|
||||
value: PortfolioRoute.Fees,
|
||||
slotBefore: <Styled.Icon iconName={IconName.Calculator} />,
|
||||
label: stringGetter({ key: STRING_KEYS.FEES }),
|
||||
href: PortfolioRoute.Fees,
|
||||
},
|
||||
{
|
||||
value: PortfolioRoute.History,
|
||||
slotBefore: <Styled.Icon iconName={IconName.History} />,
|
||||
label: stringGetter({ key: STRING_KEYS.HISTORY }),
|
||||
href: PortfolioRoute.History,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Styled.SideBar>
|
||||
<Styled.NavigationMenu
|
||||
items={[
|
||||
{
|
||||
group: 'views',
|
||||
groupLabel: stringGetter({ key: STRING_KEYS.VIEWS }),
|
||||
items: [
|
||||
{
|
||||
value: PortfolioRoute.Overview,
|
||||
slotBefore: <Styled.Icon iconName={IconName.Overview} />,
|
||||
label: stringGetter({ key: STRING_KEYS.OVERVIEW }),
|
||||
href: PortfolioRoute.Overview,
|
||||
},
|
||||
{
|
||||
value: PortfolioRoute.Positions,
|
||||
slotBefore: <Styled.Icon iconName={IconName.Cube} />,
|
||||
label: stringGetter({ key: STRING_KEYS.POSITIONS }),
|
||||
href: PortfolioRoute.Positions,
|
||||
},
|
||||
{
|
||||
value: PortfolioRoute.Orders,
|
||||
slotBefore: <Styled.Icon iconName={IconName.OrderPending} />,
|
||||
label: stringGetter({ key: STRING_KEYS.ORDERS }),
|
||||
href: PortfolioRoute.Orders,
|
||||
},
|
||||
{
|
||||
value: PortfolioRoute.Fees,
|
||||
slotBefore: <Styled.Icon iconName={IconName.Calculator} />,
|
||||
label: stringGetter({ key: STRING_KEYS.FEES }),
|
||||
href: PortfolioRoute.Fees,
|
||||
},
|
||||
{
|
||||
value: PortfolioRoute.History,
|
||||
slotBefore: <Styled.Icon iconName={IconName.History} />,
|
||||
label: stringGetter({ key: STRING_KEYS.HISTORY }),
|
||||
href: PortfolioRoute.History,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{onboardingState === OnboardingState.AccountConnected && (
|
||||
<Styled.Footer>
|
||||
<Button
|
||||
action={ButtonAction.Primary}
|
||||
onClick={() => dispatch(openDialog({ type: DialogTypes.Deposit }))}
|
||||
>
|
||||
{stringGetter({ key: STRING_KEYS.DEPOSIT })}
|
||||
</Button>
|
||||
{usdcBalance > 0 && (
|
||||
<Button
|
||||
action={ButtonAction.Base}
|
||||
onClick={() => dispatch(openDialog({ type: DialogTypes.Withdraw }))}
|
||||
>
|
||||
{stringGetter({ key: STRING_KEYS.WITHDRAW })}
|
||||
</Button>
|
||||
)}
|
||||
{(usdcBalance > 0 || nativeTokenBalance.gt(0)) && (
|
||||
<Button
|
||||
action={ButtonAction.Base}
|
||||
onClick={() => dispatch(openDialog({ type: DialogTypes.Transfer }))}
|
||||
>
|
||||
{stringGetter({ key: STRING_KEYS.TRANSFER })}
|
||||
</Button>
|
||||
)}
|
||||
</Styled.Footer>
|
||||
)}
|
||||
</Styled.SideBar>
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -139,6 +181,26 @@ Styled.PortfolioMobile = styled.div`
|
||||
${layoutMixins.expandingColumnWithHeader}
|
||||
`;
|
||||
|
||||
Styled.SideBar = styled.div`
|
||||
${layoutMixins.flexColumn}
|
||||
justify-content: space-between;
|
||||
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
Styled.Footer = styled.div`
|
||||
${layoutMixins.row}
|
||||
flex-wrap: wrap;
|
||||
|
||||
padding: 1rem;
|
||||
|
||||
gap: 0.5rem;
|
||||
|
||||
> button {
|
||||
flex-grow: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.NavigationMenu = styled(NavigationMenu)`
|
||||
padding: 0.5rem;
|
||||
padding-top: 0;
|
||||
|
||||
@@ -43,23 +43,14 @@ export const DYDXBalancePanel = () => {
|
||||
{!canAccountTrade ? (
|
||||
<OnboardingTriggerButton size={ButtonSize.Small} />
|
||||
) : (
|
||||
<>
|
||||
<Styled.ReceiveButton
|
||||
slotLeft={<Icon iconName={IconName.Qr} />}
|
||||
size={ButtonSize.Small}
|
||||
onClick={() => dispatch(openDialog({ type: DialogTypes.Receive }))}
|
||||
>
|
||||
{stringGetter({ key: STRING_KEYS.RECEIVE })}
|
||||
</Styled.ReceiveButton>
|
||||
<Button
|
||||
slotLeft={<Icon iconName={IconName.Send} />}
|
||||
size={ButtonSize.Small}
|
||||
action={ButtonAction.Primary}
|
||||
onClick={() => dispatch(openDialog({ type: DialogTypes.Transfer }))}
|
||||
>
|
||||
{stringGetter({ key: STRING_KEYS.TRANSFER })}
|
||||
</Button>
|
||||
</>
|
||||
<Button
|
||||
slotLeft={<Icon iconName={IconName.Send} />}
|
||||
size={ButtonSize.Small}
|
||||
action={ButtonAction.Primary}
|
||||
onClick={() => dispatch(openDialog({ type: DialogTypes.Transfer }))}
|
||||
>
|
||||
{stringGetter({ key: STRING_KEYS.TRANSFER })}
|
||||
</Button>
|
||||
)}
|
||||
</Styled.ReceiveAndTransferButtons>
|
||||
</Styled.Header>
|
||||
|
||||
@@ -18,9 +18,5 @@ export const tradeViewMixins: Record<
|
||||
tbody {
|
||||
font: var(--font-small-book);
|
||||
}
|
||||
|
||||
thead tr {
|
||||
box-shadow: none;
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
@@ -82,18 +82,18 @@ const MarketsDropdownContent = ({ onRowAction }: { onRowAction?: (market: string
|
||||
),
|
||||
},
|
||||
{
|
||||
columnKey: 'priceChange24H',
|
||||
getCellValue: (row) => row.priceChange24H,
|
||||
columnKey: 'priceChange24HPercent',
|
||||
getCellValue: (row) => row.priceChange24HPercent,
|
||||
label: stringGetter({ key: STRING_KEYS._24H }),
|
||||
renderCell: ({ priceChange24H, priceChange24HPercent }) => (
|
||||
renderCell: ({ priceChange24HPercent }) => (
|
||||
<Styled.InlineRow>
|
||||
{!priceChange24H ? (
|
||||
{!priceChange24HPercent ? (
|
||||
<Styled.Output type={OutputType.Text} value={null} />
|
||||
) : (
|
||||
<Styled.PriceChangeOutput
|
||||
type={OutputType.Percent}
|
||||
value={priceChange24HPercent}
|
||||
isNegative={MustBigNumber(priceChange24H).isNegative()}
|
||||
isNegative={MustBigNumber(priceChange24HPercent).isNegative()}
|
||||
/>
|
||||
)}
|
||||
</Styled.InlineRow>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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({
|
||||
@@ -39,7 +45,12 @@ export const TransferStatusSteps = ({ status, type }: ElementProps) => {
|
||||
type === 'deposit' ? STRING_KEYS.INITIATED_DEPOSIT : STRING_KEYS.INITIATED_WITHDRAWAL,
|
||||
}),
|
||||
step: TransferStatusStep.FromChain,
|
||||
link: status?.fromChain?.transactionUrl,
|
||||
link:
|
||||
type === 'deposit'
|
||||
? status?.fromChain?.transactionUrl
|
||||
: routeStatus?.[0]?.chainId === dydxChainId && routeStatus[0].txHash
|
||||
? `${mintscanTxUrl?.replace('{tx_hash}', routeStatus[0].txHash.replace('0x', ''))}`
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
label: stringGetter({ key: STRING_KEYS.BRIDGING_TOKENS }),
|
||||
@@ -54,12 +65,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.replace('0x', ''))}`
|
||||
: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
const currentStatus = routeStatus?.[routeStatus?.length - 1];
|
||||
|
||||
let currentStep = TransferStatusStep.Bridge;
|
||||
|
||||
if (!routeStatus?.length) {
|
||||
|
||||
@@ -17,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();
|
||||
|
||||
@@ -28,20 +28,18 @@ export const DepositDialog = ({ setIsOpen }: ElementProps) => {
|
||||
isOpen
|
||||
setIsOpen={setIsOpen}
|
||||
title={stringGetter({ key: STRING_KEYS.DEPOSIT })}
|
||||
description={
|
||||
!isMainnet && 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>
|
||||
{!isMainnet && showTestDeposit ? (
|
||||
<TestnetDepositForm onDeposit={closeDialog} />
|
||||
) : (
|
||||
{isMainnet || !showFaucet ? (
|
||||
<DepositForm />
|
||||
) : (
|
||||
<TestnetDepositForm onDeposit={closeDialog} />
|
||||
)}
|
||||
{!isMainnet && (
|
||||
<Styled.TextToggle onClick={() => setShowTestDeposit(!showTestDeposit)}>
|
||||
{showTestDeposit ? 'Show deposit form (Under Construction)' : 'Show test faucet'}
|
||||
<Styled.TextToggle onClick={() => setShowFaucet(!showFaucet)}>
|
||||
{showFaucet ? 'Show deposit form' : 'Show test faucet'}
|
||||
</Styled.TextToggle>
|
||||
)}
|
||||
</Styled.Content>
|
||||
|
||||
@@ -3,6 +3,7 @@ import styled, { AnyStyledComponent, css } from 'styled-components';
|
||||
|
||||
import { AnalyticsEvent } from '@/constants/analytics';
|
||||
import { STRING_KEYS } from '@/constants/localization';
|
||||
import { isMainnet } from '@/constants/networks';
|
||||
import { EvmDerivedAccountStatus, OnboardingSteps } from '@/constants/account';
|
||||
import { wallets } from '@/constants/wallets';
|
||||
|
||||
@@ -26,6 +27,7 @@ import { track } from '@/lib/analytics';
|
||||
import { AcknowledgeTerms } from './OnboardingDialog/AcknowledgeTerms';
|
||||
import { ChooseWallet } from './OnboardingDialog/ChooseWallet';
|
||||
import { GenerateKeys } from './OnboardingDialog/GenerateKeys';
|
||||
import { DepositForm } from '../forms/AccountManagementForms/DepositForm';
|
||||
|
||||
type ElementProps = {
|
||||
setIsOpen?: (open: boolean) => void;
|
||||
@@ -50,7 +52,7 @@ export const OnboardingDialog = ({ setIsOpen }: ElementProps) => {
|
||||
disconnect();
|
||||
}
|
||||
setIsOpen?.(open);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Styled.Dialog
|
||||
@@ -82,10 +84,7 @@ export const OnboardingDialog = ({ setIsOpen }: ElementProps) => {
|
||||
description: stringGetter({ key: STRING_KEYS.SIGNATURE_CREATES_COSMOS_WALLET }),
|
||||
children: (
|
||||
<Styled.Content>
|
||||
<GenerateKeys
|
||||
status={derivationStatus}
|
||||
setStatus={setDerivationStatus}
|
||||
/>
|
||||
<GenerateKeys status={derivationStatus} setStatus={setDerivationStatus} />
|
||||
</Styled.Content>
|
||||
),
|
||||
width: '23rem',
|
||||
@@ -101,14 +100,22 @@ export const OnboardingDialog = ({ setIsOpen }: ElementProps) => {
|
||||
},
|
||||
[OnboardingSteps.DepositFunds]: {
|
||||
title: stringGetter({ key: STRING_KEYS.DEPOSIT }),
|
||||
description: 'Test funds will be sent directly to your dYdX account.',
|
||||
description: !isMainnet && 'Test funds will be sent directly to your dYdX account.',
|
||||
children: (
|
||||
<Styled.Content>
|
||||
<TestnetDepositForm
|
||||
onDeposit={() => {
|
||||
track(AnalyticsEvent.TransferFaucet);
|
||||
}}
|
||||
/>
|
||||
{isMainnet ? (
|
||||
<DepositForm
|
||||
onDeposit={() => {
|
||||
track(AnalyticsEvent.TransferDeposit);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<TestnetDepositForm
|
||||
onDeposit={() => {
|
||||
track(AnalyticsEvent.TransferFaucet);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Styled.Content>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -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,7 +2,7 @@ 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 { parseUnits } from 'viem';
|
||||
|
||||
import erc20 from '@/abi/erc20.json';
|
||||
import { TransferInputField, TransferInputTokenResource, TransferType } from '@/constants/abacus';
|
||||
@@ -17,7 +17,6 @@ import { useAccounts, useDebounce, useStringGetter, useSelectedNetwork } from '@
|
||||
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';
|
||||
@@ -56,8 +55,7 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { selectedNetwork } = useSelectedNetwork();
|
||||
|
||||
const { evmAddress, signerWagmi } = useAccounts();
|
||||
const { publicClientWagmi } = useWalletConnection();
|
||||
const { evmAddress, signerWagmi, publicClientWagmi } = useAccounts();
|
||||
|
||||
const { addTransferNotification } = useLocalNotifications();
|
||||
|
||||
@@ -67,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;
|
||||
|
||||
@@ -99,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,
|
||||
@@ -173,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');
|
||||
@@ -183,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,
|
||||
@@ -195,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]);
|
||||
|
||||
@@ -230,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);
|
||||
|
||||
@@ -289,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 });
|
||||
@@ -302,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) ||
|
||||
|
||||
@@ -164,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}
|
||||
|
||||
@@ -47,7 +47,6 @@ import { MustBigNumber } from '@/lib/numbers';
|
||||
|
||||
import { TokenSelectMenu } from './TokenSelectMenu';
|
||||
import { WithdrawButtonAndReceipt } from './WithdrawForm/WithdrawButtonAndReceipt';
|
||||
import { join } from 'path';
|
||||
|
||||
export const WithdrawForm = () => {
|
||||
const stringGetter = useStringGetter();
|
||||
@@ -69,6 +68,8 @@ export const WithdrawForm = () => {
|
||||
chain: chainIdStr,
|
||||
address: toAddress,
|
||||
resources,
|
||||
errors: routeErrors,
|
||||
errorMessage: routeErrorMessage,
|
||||
} = useSelector(getTransferInputs, shallowEqual) || {};
|
||||
|
||||
const isValidAddress = toAddress && isAddress(toAddress);
|
||||
@@ -281,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))
|
||||
@@ -303,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}
|
||||
|
||||
@@ -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: (
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user